Skip to content

Commit b92a93f

Browse files
authored
Merge pull request #5 from tayloraswift/digit-grouping
digit grouping
2 parents c54ee43 + a3eb63c commit b92a93f

28 files changed

Lines changed: 379 additions & 121 deletions

Sources/D/BasisPoints.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@frozen public struct BasisPoints: DecimalPower {
2+
@inlinable public static var power: Int { 4 }
3+
@inlinable public static var sigil: String { "" }
4+
}

Sources/D/BasisPointsFormat.swift

Lines changed: 0 additions & 9 deletions
This file was deleted.

Sources/D/BigIntFormat.swift

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,41 @@
11
@frozen public struct BigIntFormat {
2-
public let stride: UInt8
2+
@usableFromInline let _stride: UInt8
33

44
@inlinable init(stride: UInt8) {
5-
self.stride = stride
5+
self._stride = stride
6+
}
7+
}
8+
extension BigIntFormat: DecimalFormat {
9+
public typealias Power = Units
10+
@inlinable public var stride: UInt8? { self._stride }
11+
@inlinable public var places: UInt8? { nil }
12+
}
13+
extension BigIntFormat {
14+
@inlinable public static func .. (
15+
self: Self,
16+
places: UInt8
17+
) -> Decimal.Grouped<Units> {
18+
.init(stride: self._stride, places: places)
19+
}
20+
21+
@inlinable public static func % (
22+
self: Self,
23+
places: UInt8
24+
) -> Decimal.Grouped<Percent> {
25+
.init(stride: self._stride, places: places)
26+
}
27+
28+
@inlinable public static func (
29+
self: Self,
30+
places: UInt8
31+
) -> Decimal.Grouped<Permille> {
32+
.init(stride: self._stride, places: places)
33+
}
34+
35+
@inlinable public static func (
36+
self: Self,
37+
places: UInt8
38+
) -> Decimal.Grouped<BasisPoints> {
39+
.init(stride: self._stride, places: places)
640
}
741
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
extension BinaryInteger {
22
@inlinable public subscript(format: BigIntFormat) -> BigIntRepresentation<Self> {
3-
.init(value: self, stride: format.stride, signed: false)
3+
.init(value: self, stride: format._stride, signed: false)
44
}
55
}

Sources/D/Decimal.Grouped.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
extension Decimal {
2+
@frozen public struct Grouped<Power> where Power: DecimalPower {
3+
@usableFromInline let _stride: UInt8
4+
@usableFromInline let _places: UInt8
5+
@inlinable init(stride: UInt8, places: UInt8) {
6+
self._stride = stride
7+
self._places = places
8+
}
9+
}
10+
}
11+
extension Decimal.Grouped: DecimalFormat {
12+
@inlinable public var stride: UInt8? { self._stride }
13+
@inlinable public var places: UInt8? { self._places }
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
extension Decimal.NaturalPrecision {
2+
/// A syntactical intermediate for constructing a ``Decimal.NaturalPrecision`` format,
3+
/// necessary to work around precedence issues with the prefix `/` operator.
4+
@frozen public struct Postfix_ {
5+
@usableFromInline let stride: UInt8
6+
@inlinable init(stride: UInt8) {
7+
self.stride = stride
8+
}
9+
}
10+
}
11+
extension Decimal.NaturalPrecision.Postfix_ {
12+
@inlinable public static prefix func / (self: Self) -> Decimal.NaturalPrecision<Power> {
13+
.init(stride: self.stride)
14+
}
15+
}
Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
extension Decimal {
2-
@frozen public enum NaturalPrecision<Format> where Format: DecimalFormat {
2+
@frozen public struct NaturalPrecision<Power> where Power: DecimalPower {
3+
@usableFromInline let _stride: UInt8
4+
@inlinable init(stride: UInt8) {
5+
self._stride = stride
6+
}
37
}
48
}
5-
extension Decimal.NaturalPrecision<UnitFormat> {
9+
extension Decimal.NaturalPrecision: DecimalFormat {
10+
@inlinable public var stride: UInt8? { self._stride }
11+
@inlinable public var places: UInt8? { nil }
12+
}
13+
extension Decimal.NaturalPrecision<Units> {
614
@inlinable public static prefix func .. (_: Self) -> () {}
715
}
8-
extension Decimal.NaturalPrecision<PercentFormat> {
16+
extension Decimal.NaturalPrecision<Percent> {
917
@inlinable public static prefix func % (_: Self) -> () {}
1018
}
11-
extension Decimal.NaturalPrecision<PermilleFormat> {
19+
extension Decimal.NaturalPrecision<Permille> {
1220
@inlinable public static prefix func (_: Self) -> () {}
1321
}
14-
extension Decimal.NaturalPrecision<BasisPointsFormat> {
22+
extension Decimal.NaturalPrecision<BasisPoints> {
1523
@inlinable public static prefix func (_: Self) -> () {}
1624
}

Sources/D/Decimal.Ungrouped.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
extension Decimal {
2+
@frozen public struct Ungrouped<Power> where Power: DecimalPower {
3+
@usableFromInline let _places: UInt8
4+
@inlinable init(places: UInt8) {
5+
self._places = places
6+
}
7+
}
8+
}
9+
extension Decimal.Ungrouped: DecimalFormat {
10+
@inlinable public var stride: UInt8? { nil }
11+
@inlinable public var places: UInt8? { self._places }
12+
}

Sources/D/Decimal.swift

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -448,12 +448,14 @@ extension Decimal: DecimalFormattable {
448448

449449
@inlinable public func format(
450450
power: Int,
451+
stride: Int? = nil,
451452
places: Int? = nil,
452453
signed: Bool = false,
453454
suffix: String = ""
454455
) -> String {
455456
let shifted: Self = .init(units: self.units, power: self.power + Int64.init(power))
456457
return shifted.format(
458+
stride: stride,
457459
places: places ?? -Int.init(min(shifted.power, 0)),
458460
signed: signed,
459461
suffix: suffix
@@ -465,6 +467,7 @@ extension Decimal: CustomStringConvertible {
465467
/// using ASCII characters only. The string contains no leading plus sign.
466468
public var description: String {
467469
self.format(
470+
stride: nil,
468471
places: -Int.init(min(self.power, 0)),
469472
signed: false,
470473
suffix: "",
@@ -494,11 +497,11 @@ extension Decimal: LosslessStringConvertible {
494497
}
495498
}
496499
extension Decimal {
497-
public func format(places: Int, signed: Bool = false, suffix: String = "") -> String {
498-
self.format(places: places, signed: signed, suffix: suffix, ascii: false)
500+
public func format(stride: Int? = nil, places: Int, signed: Bool = false, suffix: String = "") -> String {
501+
self.format(stride: stride, places: places, signed: signed, suffix: suffix, ascii: false)
499502
}
500503

501-
private func format(places: Int, signed: Bool, suffix: String, ascii: Bool) -> String {
504+
private func format(stride: Int?, places: Int, signed: Bool, suffix: String, ascii: Bool) -> String {
502505
/// We test this before we perform any rounding, to preserve the sign.
503506
let negative: Bool = self.units < 0
504507
let positive: Bool = self.units > 0
@@ -537,6 +540,25 @@ extension Decimal {
537540
zeroes.after = self.units == 0 ? places : places + Int.init(self.power)
538541
}
539542

543+
let digitsInFirstGroup: Int
544+
let digitsToGroup: Int
545+
let commas: Int
546+
547+
if zeroes.before > 0 {
548+
digitsToGroup = 1 // The leading '0' in "0.xxxxx"
549+
} else {
550+
digitsToGroup = digits + zeroes.after - places
551+
}
552+
553+
if let stride: Int = stride, 1 ..< digitsToGroup ~= stride {
554+
let r: Int
555+
(commas, r) = (digitsToGroup - 1).quotientAndRemainder(dividingBy: stride)
556+
digitsInFirstGroup = r + 1
557+
} else {
558+
commas = 0
559+
digitsInFirstGroup = digitsToGroup
560+
}
561+
540562
/// Add 1 for the decimal point. The unicode minus sign (U+2212) takes three bytes
541563
/// to encode in UTF-8.
542564
let punctuation: Int
@@ -553,7 +575,7 @@ extension Decimal {
553575
punctuation = places > 0 ? 1 : 0
554576
}
555577

556-
let characters: Int = punctuation + zeroes.before + digits + zeroes.after
578+
let characters: Int = punctuation + zeroes.before + digits + zeroes.after + commas
557579
return .init(unsafeUninitializedCapacity: characters + suffix.utf8.count) {
558580
var i: Int
559581
if negative {
@@ -572,6 +594,10 @@ extension Decimal {
572594
} else {
573595
i = 0
574596
}
597+
598+
var digitsGrouped: Int = 0
599+
var digitsInCurrentGroup: Int = digitsInFirstGroup
600+
575601
// We would only insert zeroes before, if if the decimal starts with `0.`
576602
if zeroes.before > 0 {
577603
$0[i] = 0x30 ; i += 1 // '0'
@@ -586,24 +612,47 @@ extension Decimal {
586612
$0[i] = 0x30 ; i += 1
587613
}
588614
} else if places > zeroes.after {
589-
// We know that the decimal point appears within the digits.
590-
let period: Int = characters - places - 1
591615
for utf8: UInt8 in string.utf8 {
592-
if period == i {
616+
$0[i] = utf8 ; i += 1
617+
digitsGrouped += 1
618+
619+
if digitsGrouped < digitsToGroup {
620+
digitsInCurrentGroup -= 1
621+
if let stride: Int, digitsInCurrentGroup == 0 {
622+
$0[i] = 0x2C ; i += 1 // ','
623+
digitsInCurrentGroup = stride
624+
}
625+
} else if places > 0,
626+
digitsGrouped == digitsToGroup {
593627
$0[i] = 0x2E ; i += 1 // '.'
594628
}
595-
596-
$0[i] = utf8 ; i += 1
597629
}
598630
for _: Int in 0 ..< zeroes.after {
599631
$0[i] = 0x30 ; i += 1
600632
}
601633
} else {
634+
// Decimal point appears at the end or beyond the digits.
602635
for utf8: UInt8 in string.utf8 {
603636
$0[i] = utf8 ; i += 1
637+
digitsGrouped += 1
638+
if digitsGrouped < digitsToGroup {
639+
digitsInCurrentGroup -= 1
640+
if let stride: Int, digitsInCurrentGroup == 0 {
641+
$0[i] = 0x2C ; i += 1 // ','
642+
digitsInCurrentGroup = stride
643+
}
644+
}
604645
}
605646
for _: Int in 0 ..< zeroes.after - places {
606647
$0[i] = 0x30 ; i += 1
648+
digitsGrouped += 1
649+
if digitsGrouped < digitsToGroup {
650+
digitsInCurrentGroup -= 1
651+
if let stride: Int, digitsInCurrentGroup == 0 {
652+
$0[i] = 0x2C ; i += 1 // ','
653+
digitsInCurrentGroup = stride
654+
}
655+
}
607656
}
608657
if places > 0 {
609658
$0[i] = 0x2E ; i += 1 // '.'

Sources/D/DecimalFormat.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
public protocol DecimalFormat {
2-
/// The power of ten offset to apply for this format. For example, when formatting percents,
3-
/// this is 2, because we want to print the number `0.XYZ` as `XY.Z%`.
4-
static var power: Int { get }
5-
static var sigil: String { get }
1+
public protocol DecimalFormat<Power> {
2+
associatedtype Power: DecimalPower
3+
/// The grouping stride to use when formatting. If `nil`, no grouping will be applied.
4+
var stride: UInt8? { get }
65
/// The number of decimal places to use when formatting. If set to 0 or less, no decimal
76
/// places will appear.
8-
var places: UInt8 { get }
7+
var places: UInt8? { get }
98
}

0 commit comments

Comments
 (0)