Improve Number Formatting (#1266)

* Improves number formatting (IOS-246)

* Implement formatting > 1T (IOS-246)

* Fix typo (IOS-246)

* Update MastodonTests/MetricFormatterTests.swift

Co-authored-by: Nathan Mattes <hallo@bullenscheisse.de>

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

---------

Co-authored-by: Nathan Mattes <hallo@bullenscheisse.de>
This commit is contained in:
Marcus Kida 2024-04-04 16:55:19 +02:00 committed by GitHub
parent 5925436bc5
commit cc9faf5aea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 188 additions and 13 deletions

View File

@ -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 */; };
@ -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; };
@ -2115,6 +2117,7 @@
isa = PBXGroup;
children = (
DB427DEC25BAA00100D1B89D /* MastodonTests.swift */,
2A8DCC602BBEA6DE00B2A4EC /* MetricFormatterTests.swift */,
DB427DEE25BAA00100D1B89D /* Info.plist */,
);
path = MastodonTests;
@ -3848,6 +3851,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2A8DCC612BBEA6DE00B2A4EC /* MetricFormatterTests.swift in Sources */,
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -8,6 +8,7 @@
}
},
{
"enabled" : false,
"id" : "06DB4A21-6DDF-454F-ABEE-E77DD7AAA146",
"name" : "Arabic",
"options" : {
@ -15,6 +16,7 @@
}
},
{
"enabled" : false,
"id" : "D95412BD-821C-4FE5-9DA6-8D77C0972B72",
"name" : "Catalan",
"options" : {
@ -22,6 +24,7 @@
}
},
{
"enabled" : false,
"id" : "EE9219EA-2011-48B5-A475-309C99F91D6D",
"name" : "Chinese, Simplified",
"options" : {
@ -29,6 +32,7 @@
}
},
{
"enabled" : false,
"id" : "6FBBAF31-D445-482E-B67B-271F8216AEB6",
"name" : "Dutch",
"options" : {
@ -36,6 +40,7 @@
}
},
{
"enabled" : false,
"id" : "45A93B8D-54C5-4906-8AC6-5B6FE561CA25",
"name" : "French",
"options" : {
@ -43,6 +48,7 @@
}
},
{
"enabled" : false,
"id" : "8344111A-3025-4CA0-838C-AF94EBA4D4BE",
"name" : "German",
"options" : {
@ -50,6 +56,7 @@
}
},
{
"enabled" : false,
"id" : "4EE64E47-F9E5-4189-8571-20D29941F854",
"name" : "Japanese",
"options" : {
@ -57,6 +64,7 @@
}
},
{
"enabled" : false,
"id" : "746F1EBA-E12B-40C4-85C6-A14DC61A180B",
"name" : "Spanish",
"options" : {
@ -64,6 +72,7 @@
}
},
{
"enabled" : false,
"id" : "EDA29FF5-1F0E-451A-863D-0899CE07CB09",
"name" : "Spanish (Latin America)",
"options" : {
@ -76,7 +85,11 @@
},
"testTargets" : [
{
"enabled" : false,
"skippedTests" : [
"MastodonTests",
"MastodonTests\/testConnectOnion()",
"MastodonTests\/testWebFinger()"
],
"target" : {
"containerPath" : "container:Mastodon.xcodeproj",
"identifier" : "DB427DE725BAA00100D1B89D",

View File

@ -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 ? "" : "-"
@ -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

View File

@ -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")
}
}