Skip to content

Commit 03580fb

Browse files
author
Reed Es
committed
reworked implementation for efficiency & flexibility
1 parent 17faa49 commit 03580fb

File tree

4 files changed

+168
-138
lines changed

4 files changed

+168
-138
lines changed

Sources/NumberCompactor.swift

Lines changed: 71 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,13 @@ import Foundation
2020

2121
public class NumberCompactor: NumberFormatter {
2222

23-
public let blankIfZero: Bool
24-
public let roundSmallToWhole: Bool
25-
26-
let threshold: Double
27-
let netKiloExtent: Double
28-
let netMegaExtent: Double
29-
let netGigaExtent: Double
30-
let netTeraExtent: Double
31-
let netPetaExtent: Double
32-
let netExaExtent: Double
23+
public var blankIfZero: Bool
24+
public var roundSmallToWhole: Bool
3325

3426
public init(blankIfZero: Bool = false,
3527
roundSmallToWhole: Bool = false) {
3628
self.blankIfZero = blankIfZero
3729
self.roundSmallToWhole = roundSmallToWhole
38-
self.threshold = roundSmallToWhole ? 0.5 : 0.05
39-
self.netKiloExtent = Scale.kilo.extent - threshold
40-
self.netMegaExtent = Scale.mega.extent - threshold * Scale.kilo.extent
41-
self.netGigaExtent = Scale.giga.extent - threshold * Scale.mega.extent
42-
self.netTeraExtent = Scale.tera.extent - threshold * Scale.giga.extent
43-
self.netPetaExtent = Scale.peta.extent - threshold * Scale.tera.extent
44-
self.netExaExtent = Scale.exa.extent - threshold * Scale.peta.extent
4530
super.init()
4631
}
4732

@@ -50,59 +35,88 @@ public class NumberCompactor: NumberFormatter {
5035
}
5136

5237
public override func string(from value: NSNumber) -> String? {
53-
let absValue = abs(Double(truncating: value))
38+
let rawValue: Double = Double(truncating: value)
39+
let absValue = abs(rawValue)
40+
let threshold = NumberCompactor.getThreshold(roundSmallToWhole)
5441

5542
if blankIfZero, absValue <= threshold { return "" }
43+
44+
let (scaledValue, scaleSymbol) = NumberCompactor.getScaledValue(rawValue, roundSmallToWhole)
5645

57-
var netValue = Double(truncating: value)
58-
var scaleSymbol: Scale = .none
59-
60-
switch absValue {
61-
case 0.0 ... threshold:
62-
// if inside threshold, drop the fraction, to avoid awkward "-$0"
63-
netValue = 0.0
64-
case threshold ..< netKiloExtent:
65-
_ = 0 // verbatim netValue
66-
case netKiloExtent ..< netMegaExtent:
67-
netValue /= Scale.kilo.extent
68-
scaleSymbol = .kilo
69-
case netMegaExtent ..< netGigaExtent:
70-
netValue /= Scale.mega.extent
71-
scaleSymbol = .mega
72-
case netGigaExtent ..< netTeraExtent:
73-
netValue /= Scale.giga.extent
74-
scaleSymbol = .giga
75-
case netTeraExtent ..< netPetaExtent:
76-
netValue /= Scale.tera.extent
77-
scaleSymbol = .tera
78-
case netPetaExtent ..< netExaExtent:
79-
netValue /= Scale.peta.extent
80-
scaleSymbol = .peta
81-
default:
82-
netValue /= Scale.exa.extent
83-
scaleSymbol = .exa
84-
}
85-
86-
let smallValueThreshold = 100 - threshold
87-
let isSmallAbsValue = absValue < smallValueThreshold
88-
let isLargeNetValue = smallValueThreshold <= abs(netValue)
89-
let roundToWhole = isSmallAbsValue && roundSmallToWhole
90-
let fractionDigitCount = roundToWhole || isLargeNetValue ? 0 : 1
46+
let showWholeValue: Bool = {
47+
let smallValueThreshold = 100 - threshold
48+
if smallValueThreshold <= abs(scaledValue) { return true }
49+
let isSmallAbsValue = absValue < smallValueThreshold
50+
return isSmallAbsValue && roundSmallToWhole
51+
}()
9152

53+
let fractionDigitCount = showWholeValue ? 0 : 1
9254
self.maximumFractionDigits = fractionDigitCount
9355
self.minimumFractionDigits = fractionDigitCount
94-
self.usesGroupingSeparator = false
95-
96-
guard let raw = super.string(from: netValue as NSNumber) else { return nil }
9756

98-
guard let lastDigitIndex = raw.lastIndex(where: { $0.isNumber }) else { return nil }
57+
guard let raw = super.string(from: scaledValue as NSNumber),
58+
let lastDigitIndex = raw.lastIndex(where: { $0.isNumber })
59+
else { return nil }
9960

10061
let afterLastDigitIndex = raw.index(after: lastDigitIndex)
101-
10262
let prefix = raw.prefix(upTo: afterLastDigitIndex)
10363
let suffix = raw.suffix(from: afterLastDigitIndex)
10464

10565
return "\(prefix)\(scaleSymbol.abbreviation)\(suffix)"
10666
}
10767
}
10868

69+
extension NumberCompactor {
70+
71+
private typealias LOOKUP = (range: Range<Double>, divisor: Double, scale: Scale)
72+
73+
// thresholds
74+
private static let halfDollar: Double = 0.5
75+
private static let nickel: Double = 0.05
76+
77+
// cached lookup tables
78+
private static let halfDollarLookup: [LOOKUP] = NumberCompactor.generateLookup(threshold: halfDollar)
79+
private static let nickelLookup: [LOOKUP] = NumberCompactor.generateLookup(threshold: nickel)
80+
81+
static func getThreshold(_ roundSmallToWhole: Bool) -> Double {
82+
roundSmallToWhole ? NumberCompactor.halfDollar : NumberCompactor.nickel
83+
}
84+
85+
static func getScaledValue(_ rawValue: Double, _ roundSmallToWhole: Bool) -> (Double, Scale) {
86+
let threshold = getThreshold(roundSmallToWhole)
87+
let absValue = abs(rawValue)
88+
if !(0.0...threshold).contains(absValue) {
89+
if let (divisor, scale) = NumberCompactor.lookup(roundSmallToWhole, absValue) {
90+
let netValue = rawValue / divisor
91+
return (netValue, scale)
92+
}
93+
}
94+
return (0.0, .none)
95+
}
96+
97+
private static func lookup(_ roundSmallToWhole: Bool, _ absValue: Double) -> (divisor: Double, scale: Scale)? {
98+
let records = roundSmallToWhole ? NumberCompactor.halfDollarLookup : NumberCompactor.nickelLookup
99+
guard let record = records.first(where: { $0.range.contains(absValue) }) else { return nil }
100+
return (record.divisor, record.scale)
101+
}
102+
103+
private static func generateLookup(threshold: Double) -> [LOOKUP] {
104+
let netKiloExtent: Double = Scale.kilo.extent - threshold
105+
let netMegaExtent: Double = Scale.mega.extent - threshold * Scale.kilo.extent
106+
let netGigaExtent: Double = Scale.giga.extent - threshold * Scale.mega.extent
107+
let netTeraExtent: Double = Scale.tera.extent - threshold * Scale.giga.extent
108+
let netPetaExtent: Double = Scale.peta.extent - threshold * Scale.tera.extent
109+
let netExaExtent : Double = Scale.exa.extent - threshold * Scale.peta.extent
110+
111+
return [
112+
(threshold ..< netKiloExtent, 1.0, .none),
113+
(netKiloExtent ..< netMegaExtent, Scale.kilo.extent, .kilo),
114+
(netMegaExtent ..< netGigaExtent, Scale.mega.extent, .mega),
115+
(netGigaExtent ..< netTeraExtent, Scale.giga.extent, .giga),
116+
(netTeraExtent ..< netPetaExtent, Scale.tera.extent, .tera),
117+
(netPetaExtent ..< netExaExtent, Scale.peta.extent, .peta),
118+
(netExaExtent ..< Double.greatestFiniteMagnitude, Scale.exa.extent, .exa),
119+
]
120+
}
121+
}
122+

Sources/TimeCompactor.swift

Lines changed: 71 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,16 @@ import Foundation
2020

2121
public class TimeCompactor: NumberFormatter {
2222

23-
public let blankIfZero: Bool
24-
public let style: Style
25-
public let roundSmallToWhole: Bool
23+
public var blankIfZero: Bool
24+
public var style: Style
25+
public var roundSmallToWhole: Bool
2626

27-
let threshold: TimeInterval
28-
let netMinuteExtent: TimeInterval
29-
let netHourExtent: TimeInterval
30-
let netDayExtent: TimeInterval
31-
let netYearExtent: TimeInterval
32-
let netCenturyExtent: TimeInterval
33-
let netMilleniumExtent: TimeInterval
34-
3527
public init(blankIfZero: Bool = false,
3628
style: Style = .short,
3729
roundSmallToWhole: Bool = false) {
3830
self.blankIfZero = blankIfZero
3931
self.style = style
4032
self.roundSmallToWhole = roundSmallToWhole
41-
self.threshold = roundSmallToWhole ? 0.5 : 0.05
42-
self.netMinuteExtent = Scale.minute.extent - threshold
43-
self.netHourExtent = Scale.hour.extent - threshold * Scale.minute.extent
44-
self.netDayExtent = Scale.day.extent - threshold * Scale.hour.extent
45-
self.netYearExtent = Scale.year.extent - threshold * Scale.day.extent
46-
self.netCenturyExtent = Scale.century.extent - threshold * Scale.year.extent
47-
self.netMilleniumExtent = Scale.millenium.extent - threshold * Scale.century.extent
4833
super.init()
4934
}
5035

@@ -53,55 +38,30 @@ public class TimeCompactor: NumberFormatter {
5338
}
5439

5540
public override func string(from value: NSNumber) -> String? {
56-
let absValue = abs(TimeInterval(truncating: value))
41+
let rawValue: Double = Double(truncating: value)
42+
let absValue = abs(rawValue)
43+
let threshold = TimeCompactor.getThreshold(roundSmallToWhole)
5744

5845
if blankIfZero, absValue <= threshold { return "" }
5946

60-
var scaleSymbol: Scale = .second
61-
var netValue = TimeInterval(truncating: value)
62-
63-
switch absValue {
64-
case 0.0 ... threshold:
65-
// if inside threshold, drop the fraction, to avoid awkward "-0s"
66-
netValue = 0.0
67-
case threshold ..< netMinuteExtent:
68-
_ = 0 // verbatim netValue
69-
case netMinuteExtent ..< netHourExtent:
70-
netValue /= Scale.minute.extent
71-
scaleSymbol = .minute
72-
case netHourExtent ..< netDayExtent:
73-
netValue /= Scale.hour.extent
74-
scaleSymbol = .hour
75-
case netDayExtent ..< netYearExtent:
76-
netValue /= Scale.day.extent
77-
scaleSymbol = .day
78-
case netYearExtent ..< netCenturyExtent:
79-
netValue /= Scale.year.extent
80-
scaleSymbol = .year
81-
case netCenturyExtent ..< netMilleniumExtent:
82-
netValue /= Scale.century.extent
83-
scaleSymbol = .century
84-
default:
85-
netValue /= Scale.millenium.extent
86-
scaleSymbol = .millenium
87-
}
47+
let (scaledValue, scaleSymbol) = TimeCompactor.getScaledValue(rawValue, roundSmallToWhole)
48+
49+
let showWholeValue: Bool = {
50+
let smallValueThreshold = 100 - threshold
51+
let isLargeNetValue = smallValueThreshold <= abs(scaledValue)
52+
let roundToWhole = !isLargeNetValue && roundSmallToWhole
53+
return roundToWhole || isLargeNetValue
54+
}()
8855

89-
let smallValueThreshold = 100 - threshold
90-
let isLargeNetValue = smallValueThreshold <= abs(netValue)
91-
let roundToWhole = !isLargeNetValue && roundSmallToWhole
92-
let fractionDigitCount = roundToWhole || isLargeNetValue ? 0 : 1
93-
94-
self.numberStyle = .decimal
56+
let fractionDigitCount = showWholeValue ? 0 : 1
9557
self.minimumFractionDigits = fractionDigitCount
9658
self.maximumFractionDigits = fractionDigitCount
97-
self.usesGroupingSeparator = false
9859

99-
guard let raw = super.string(from: netValue as NSNumber) else { return nil }
100-
101-
guard let lastDigitIndex = raw.lastIndex(where: { $0.isNumber }) else { return nil }
60+
guard let raw = super.string(from: scaledValue as NSNumber),
61+
let lastDigitIndex = raw.lastIndex(where: { $0.isNumber })
62+
else { return nil }
10263

10364
let afterLastDigitIndex = raw.index(after: lastDigitIndex)
104-
10565
let prefix = raw.prefix(upTo: afterLastDigitIndex)
10666

10767
switch style {
@@ -114,3 +74,56 @@ public class TimeCompactor: NumberFormatter {
11474
}
11575
}
11676
}
77+
78+
extension TimeCompactor {
79+
private typealias LOOKUP = (range: Range<Double>, divisor: Double, scale: Scale)
80+
81+
// thresholds
82+
private static let halfDollar: Double = 0.5
83+
private static let nickel: Double = 0.05
84+
85+
// cached lookup tables
86+
private static let halfDollarLookup: [LOOKUP] = TimeCompactor.generateLookup(threshold: halfDollar)
87+
private static let nickelLookup: [LOOKUP] = TimeCompactor.generateLookup(threshold: nickel)
88+
89+
static func getThreshold(_ roundSmallToWhole: Bool) -> Double {
90+
roundSmallToWhole ? TimeCompactor.halfDollar : TimeCompactor.nickel
91+
}
92+
93+
static func getScaledValue(_ rawValue: Double, _ roundSmallToWhole: Bool) -> (Double, Scale) {
94+
let threshold = getThreshold(roundSmallToWhole)
95+
let absValue = abs(rawValue)
96+
if !(0.0...threshold).contains(absValue) {
97+
if let (divisor, scale) = TimeCompactor.lookup(roundSmallToWhole, absValue) {
98+
let netValue = rawValue / divisor
99+
return (netValue, scale)
100+
}
101+
}
102+
return (0.0, .second)
103+
}
104+
105+
private static func lookup(_ roundSmallToWhole: Bool, _ absValue: Double) -> (divisor: Double, scale: Scale)? {
106+
let records = roundSmallToWhole ? TimeCompactor.halfDollarLookup : TimeCompactor.nickelLookup
107+
guard let record = records.first(where: { $0.range.contains(absValue) }) else { return nil }
108+
return (record.divisor, record.scale)
109+
}
110+
111+
private static func generateLookup(threshold: Double) -> [LOOKUP] {
112+
let netMinuteExtent: Double = Scale.minute.extent - threshold
113+
let netHourExtent: Double = Scale.hour.extent - threshold * Scale.minute.extent
114+
let netDayExtent: Double = Scale.day.extent - threshold * Scale.hour.extent
115+
let netYearExtent: Double = Scale.year.extent - threshold * Scale.day.extent
116+
let netCenturyExtent: Double = Scale.century.extent - threshold * Scale.year.extent
117+
let netMilleniumExtent : Double = Scale.millenium.extent - threshold * Scale.century.extent
118+
119+
return [
120+
(threshold ..< netMinuteExtent, 1.0, .second),
121+
(netMinuteExtent ..< netHourExtent, Scale.minute.extent, .minute),
122+
(netHourExtent ..< netDayExtent, Scale.hour.extent, .hour),
123+
(netDayExtent ..< netYearExtent, Scale.day.extent, .day),
124+
(netYearExtent ..< netCenturyExtent, Scale.year.extent, .year),
125+
(netCenturyExtent ..< netMilleniumExtent, Scale.century.extent, .century),
126+
(netMilleniumExtent ..< Double.greatestFiniteMagnitude, Scale.millenium.extent, .millenium),
127+
]
128+
}
129+
}

Tests/CurrencyCompactorTests.swift

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -144,28 +144,28 @@ class CurrencyCompactorTests: XCTestCase {
144144
c.currencyCode = "EUR"
145145
c.currencyDecimalSeparator = ","
146146

147-
XCTAssertEqual("-€ 12,1k", c.string(from: -12050.01))
147+
XCTAssertEqual("-€12,1k", c.string(from: -12050.01))
148148

149-
XCTAssertEqual(" 0", c.string(from: 0.000))
150-
XCTAssertEqual(" 0", c.string(from: 0.050))
151-
XCTAssertEqual(" 0", c.string(from: 0.051))
152-
XCTAssertEqual(" 0", c.string(from: 0.250))
153-
XCTAssertEqual(" 0", c.string(from: 0.251))
154-
XCTAssertEqual(" 1", c.string(from: 1.250))
155-
XCTAssertEqual(" 1", c.string(from: 1.251))
156-
XCTAssertEqual(" 12", c.string(from: 12.050))
157-
XCTAssertEqual(" 12", c.string(from: 12.051))
158-
XCTAssertEqual(" 120", c.string(from: 120.50))
159-
XCTAssertEqual(" 121", c.string(from: 120.51))
160-
XCTAssertEqual(" 12,0k", c.string(from: 12050.00))
161-
XCTAssertEqual(" 12,1k", c.string(from: 12050.01))
162-
XCTAssertEqual(" 120k", c.string(from: 120_500.00))
163-
XCTAssertEqual(" 121k", c.string(from: 120_500.01))
164-
XCTAssertEqual(" 120M", c.string(from: 120_500_000.00))
165-
XCTAssertEqual(" 121M", c.string(from: 120_500_000.01))
166-
XCTAssertEqual(" 120G", c.string(from: 120_500_000_000.00))
167-
XCTAssertEqual(" 121G", c.string(from: 120_500_000_000.01))
168-
XCTAssertEqual(" 120T", c.string(from: 120_500_000_000_000.00))
169-
XCTAssertEqual(" 121T", c.string(from: 120_500_000_000_000.01))
149+
XCTAssertEqual("€0", c.string(from: 0.000))
150+
XCTAssertEqual("€0", c.string(from: 0.050))
151+
XCTAssertEqual("€0", c.string(from: 0.051))
152+
XCTAssertEqual("€0", c.string(from: 0.250))
153+
XCTAssertEqual("€0", c.string(from: 0.251))
154+
XCTAssertEqual("€1", c.string(from: 1.250))
155+
XCTAssertEqual("€1", c.string(from: 1.251))
156+
XCTAssertEqual("€12", c.string(from: 12.050))
157+
XCTAssertEqual("€12", c.string(from: 12.051))
158+
XCTAssertEqual("€120", c.string(from: 120.50))
159+
XCTAssertEqual("€121", c.string(from: 120.51))
160+
XCTAssertEqual("€12,0k", c.string(from: 12050.00))
161+
XCTAssertEqual("€12,1k", c.string(from: 12050.01))
162+
XCTAssertEqual("€120k", c.string(from: 120_500.00))
163+
XCTAssertEqual("€121k", c.string(from: 120_500.01))
164+
XCTAssertEqual("€120M", c.string(from: 120_500_000.00))
165+
XCTAssertEqual("€121M", c.string(from: 120_500_000.01))
166+
XCTAssertEqual("€120G", c.string(from: 120_500_000_000.00))
167+
XCTAssertEqual("€121G", c.string(from: 120_500_000_000.01))
168+
XCTAssertEqual("€120T", c.string(from: 120_500_000_000_000.00))
169+
XCTAssertEqual("€121T", c.string(from: 120_500_000_000_000.01))
170170
}
171171
}

Tests/NumberCompactorTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ class NumberCompactorTests: XCTestCase {
3939
}
4040

4141
func testWholeNumber() {
42-
let c = NumberCompactor(blankIfZero: false, roundSmallToWhole: true)
42+
let c = NumberCompactor()
43+
c.roundSmallToWhole = true
4344

4445
XCTAssertEqual("8", c.string(from: 8))
4546
XCTAssertEqual("-8", c.string(from: -8))
@@ -56,6 +57,8 @@ class NumberCompactorTests: XCTestCase {
5657
XCTAssertEqual("100", c.string(from: 99.50))
5758
XCTAssertEqual("-100", c.string(from: -99.50))
5859

60+
XCTAssertEqual("999", c.string(from: 999))
61+
5962
XCTAssertEqual("999", c.string(from: 999.49))
6063
XCTAssertEqual("-999", c.string(from: -999.49))
6164

0 commit comments

Comments
 (0)