Skip to content

Commit

Permalink
Improve Number Formatting (#1266)
Browse files Browse the repository at this point in the history
* Improves number formatting (IOS-246)

* Implement formatting > 1T (IOS-246)

* Fix typo (IOS-246)

* Update MastodonTests/MetricFormatterTests.swift

Co-authored-by: Nathan Mattes <[email protected]>

* Improve decimal formatting and add tests (IOS-246)

---------

Co-authored-by: Nathan Mattes <[email protected]>
  • Loading branch information
kimar and zeitschlag authored Apr 4, 2024
1 parent 5925436 commit cc9faf5
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 13 deletions.
4 changes: 4 additions & 0 deletions Mastodon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */; };
2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */; };
2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */; };
2A8DCC612BBEA6DE00B2A4EC /* MetricFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8DCC602BBEA6DE00B2A4EC /* MetricFormatterTests.swift */; };
2A90A157296EEE500026C155 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A90A156296EEE500026C155 /* MastodonSDKDynamic */; };
2A9D0664298C048800BF38CB /* LatestFollowersWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */; };
2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */; };
Expand Down Expand Up @@ -652,6 +653,7 @@
2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountIntentHandler.swift; sourceTree = "<group>"; };
2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountWidget.swift; sourceTree = "<group>"; };
2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountWidgetView.swift; sourceTree = "<group>"; };
2A8DCC602BBEA6DE00B2A4EC /* MetricFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricFormatterTests.swift; sourceTree = "<group>"; };
2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestFollowersWidget.swift; sourceTree = "<group>"; };
2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestFollowersWidgetView.swift; sourceTree = "<group>"; };
2AAAA34D2B04DE21004C6672 /* VisionKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VisionKit.framework; path = System/Library/Frameworks/VisionKit.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -2115,6 +2117,7 @@
isa = PBXGroup;
children = (
DB427DEC25BAA00100D1B89D /* MastodonTests.swift */,
2A8DCC602BBEA6DE00B2A4EC /* MetricFormatterTests.swift */,
DB427DEE25BAA00100D1B89D /* Info.plist */,
);
path = MastodonTests;
Expand Down Expand Up @@ -3848,6 +3851,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2A8DCC612BBEA6DE00B2A4EC /* MetricFormatterTests.swift in Sources */,
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
15 changes: 14 additions & 1 deletion Mastodon/Mastodon.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -8,62 +8,71 @@
}
},
{
"enabled" : false,
"id" : "06DB4A21-6DDF-454F-ABEE-E77DD7AAA146",
"name" : "Arabic",
"options" : {
"language" : "ar"
}
},
{
"enabled" : false,
"id" : "D95412BD-821C-4FE5-9DA6-8D77C0972B72",
"name" : "Catalan",
"options" : {
"language" : "ca"
}
},
{
"enabled" : false,
"id" : "EE9219EA-2011-48B5-A475-309C99F91D6D",
"name" : "Chinese, Simplified",
"options" : {
"language" : "zh-Hans"
}
},
{
"enabled" : false,
"id" : "6FBBAF31-D445-482E-B67B-271F8216AEB6",
"name" : "Dutch",
"options" : {
"language" : "nl"
}
},
{
"enabled" : false,
"id" : "45A93B8D-54C5-4906-8AC6-5B6FE561CA25",
"name" : "French",
"options" : {
"language" : "fr"
}
},
{
"enabled" : false,
"id" : "8344111A-3025-4CA0-838C-AF94EBA4D4BE",
"name" : "German",
"options" : {
"language" : "de"
}
},
{
"enabled" : false,
"id" : "4EE64E47-F9E5-4189-8571-20D29941F854",
"name" : "Japanese",
"options" : {
"language" : "ja"
}
},
{
"enabled" : false,
"id" : "746F1EBA-E12B-40C4-85C6-A14DC61A180B",
"name" : "Spanish",
"options" : {
"language" : "es"
}
},
{
"enabled" : false,
"id" : "EDA29FF5-1F0E-451A-863D-0899CE07CB09",
"name" : "Spanish (Latin America)",
"options" : {
Expand All @@ -76,7 +85,11 @@
},
"testTargets" : [
{
"enabled" : false,
"skippedTests" : [
"MastodonTests",
"MastodonTests\/testConnectOnion()",
"MastodonTests\/testWebFinger()"
],
"target" : {
"containerPath" : "container:Mastodon.xcodeproj",
"identifier" : "DB427DE725BAA00100D1B89D",
Expand Down
56 changes: 44 additions & 12 deletions MastodonSDK/Sources/MastodonUI/Helper/MastodonMetricFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,30 @@

import Foundation

public final class MastodonMetricFormatter: Formatter {
enum DecimalUnit: Int {
case one = 1
case ten = 10
case hundred = 100
case thousand = 1_000
case million = 1_000_000
case billion = 1_000_000_000
case trillion = 1_000_000_000_000

var asInt: Int {
self.rawValue
}

var asDouble: Double {
Double(self.rawValue)
}
}


public final class MastodonMetricFormatter: Formatter {

private let ten_thousands = DecimalUnit.thousand.asInt * 10
private let ten_millions = DecimalUnit.million.asInt * 10

public func string(from number: Int) -> String? {
let isPositive = number >= 0
let symbol = isPositive ? "" : "-"
Expand All @@ -19,20 +41,30 @@ public final class MastodonMetricFormatter: Formatter {
let metric: String

switch value {
case 0..<1000: // 0 ~ 1K
metric = String(value)
case 1000..<10000: // 1K ~ 10K
numberFormatter.maximumFractionDigits = 1
let string = numberFormatter.string(from: NSNumber(value: Double(value) / 1000.0)) ?? String(value / 1000)
metric = string + "K"
case 10000..<1000000: // 10K ~ 1M
case 0 ..< DecimalUnit.thousand.asInt: // 0 ~ 1K
numberFormatter.maximumFractionDigits = 0
let string = numberFormatter.string(from: NSNumber(value: Double(value) / 1000.0)) ?? String(value / 1000)
let string = numberFormatter.string(from: NSNumber(value: value)) ?? String(value)
metric = string
case DecimalUnit.thousand.asInt ..< DecimalUnit.million.asInt: // 1K ~ 1M
numberFormatter.maximumFractionDigits = value < ten_thousands ? 1 : 0
let string = numberFormatter.string(from: NSNumber(value: Double(value) / DecimalUnit.thousand.asDouble)) ??
String(value / DecimalUnit.thousand.asInt)
metric = string + "K"
default:
numberFormatter.maximumFractionDigits = 0
let string = numberFormatter.string(from: NSNumber(value: Double(value) / 1000000.0)) ?? String(value / 1000000)
case DecimalUnit.million.asInt ..< DecimalUnit.billion.asInt: // 1M ~ 1B
numberFormatter.maximumFractionDigits = value < ten_millions ? 1 : 0
let string = numberFormatter.string(from: NSNumber(value: Double(value) / DecimalUnit.million.asDouble)) ??
String(value / DecimalUnit.million.asInt)
metric = string + "M"
case DecimalUnit.billion.asInt ..< DecimalUnit.trillion.asInt: // 1B ~ 1T
numberFormatter.maximumFractionDigits = 0
let string = numberFormatter.string(from: NSNumber(value: Double(value) / DecimalUnit.billion.asDouble)) ??
String(value / DecimalUnit.billion.asInt)
metric = string + "B"
default: // > 1T
numberFormatter.maximumFractionDigits = 0
let string = numberFormatter.string(from: NSNumber(value: Double(value) / DecimalUnit.trillion.asDouble)) ??
String(value / DecimalUnit.trillion.asInt)
metric = string + "T"
}

return symbol + metric
Expand Down
126 changes: 126 additions & 0 deletions MastodonTests/MetricFormatterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.

import XCTest
@testable import MastodonUI

class MetricFormatterTests: XCTestCase {

func test_tensFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 12)

XCTAssertEqual(value, "12")
}

func test_hundredsFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 123)

XCTAssertEqual(value, "123")
}

func test_thousandOneFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 1001)

XCTAssertEqual(value, "1K")
}

func test_thousandFiftyFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 1050)

XCTAssertEqual(value, "1K")
}

func test_thousandNinetynineFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 1099)

XCTAssertEqual(value, "1,1K")
}

func test_thousandNinehundredFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 1900)

XCTAssertEqual(value, "1,9K")
}

func test_thousandsFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 1234)

XCTAssertEqual(value, "1,2K")
}

func test_sixThousandsFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 6666)

XCTAssertEqual(value, "6,7K")
}

func test_millionsFormat_oneTwoThreeMillion() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 1_234_567)

XCTAssertEqual(value, "1,2M")
}

func test_millionsFormat_exactlyTenMillion() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 10_000_000)

XCTAssertEqual(value, "10M")
}

func test_millionsFormat_twelveOneTwoThreeMillion() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 12_345_789)

XCTAssertEqual(value, "12M")
}

func test_billionsFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 10_000_000_000)

XCTAssertEqual(value, "10B")
}

func test_billionsFormat_oneTwoThreeBillion() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 12_345_678_912)

XCTAssertEqual(value, "12B")
}

func test_trillionsFormat() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 10_000_000_000_000)

XCTAssertEqual(value, "10T")
}

func test_trillionsFormat_oneTwoThreeTrillion() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 12_345_678_912_345)

XCTAssertEqual(value, "12T")
}

func test_trillionsFormat_oneTwoThree_youGottaBeKiddinMeTrillion() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 12_345_678_912_345_678)

XCTAssertEqual(value, "12346T")
}

func test_trillionsFormat_oneTwoThree_lastDigitBeforeIntegerOverflowTrillion() {
let formatter = MastodonMetricFormatter()
let value = formatter.string(from: 12_345_678_912_345_678_91)

XCTAssertEqual(value, "1234568T")
}
}

0 comments on commit cc9faf5

Please sign in to comment.