mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-02-02 18:36:44 +01:00
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:
parent
5925436bc5
commit
cc9faf5aea
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
126
MastodonTests/MetricFormatterTests.swift
Normal file
126
MastodonTests/MetricFormatterTests.swift
Normal 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")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user