1
+ import Abbr , { type QuantityAbbrSymbol , ABBR_SYMBOLS_ARRAY } from './number-abbreviate'
2
+
3
+ interface FormatThreshold {
4
+ from : number
5
+ use : QuantityAbbrSymbol
6
+ }
7
+
8
+ const formatAndAbbreviateAsCurrency = (
9
+ n : number | null ,
10
+ thresholds : FormatThreshold [ ] = [ {
11
+ from : 1000000000 ,
12
+ use : 'M'
13
+ } ] ,
14
+ /**
15
+ * Chars that will be added by ui if the number is rounded.
16
+ * For example, if the desired output for 10.15 is "~10.1",
17
+ * the tilda counts as 1 char.
18
+ */
19
+ roundingAdds : number = 1 ,
20
+ maxDecimal : number = 2
21
+ ) : {
22
+ full : string
23
+ result : string
24
+ change : 'rounded' | 'none' | 'abbr' | 'empty'
25
+ } => {
26
+ if ( n === null ) {
27
+ return {
28
+ full : '' ,
29
+ result : '' ,
30
+ change : 'empty'
31
+ }
32
+ }
33
+
34
+ const usdFormatter = Intl . NumberFormat ( 'en-US' , {
35
+ style : 'currency' ,
36
+ currency : 'USD' ,
37
+ minimumFractionDigits : 0
38
+ } )
39
+ const formatted = usdFormatter . format ( n )
40
+
41
+ if ( n < thresholds [ 0 ] . from ) {
42
+ return {
43
+ full : formatted ,
44
+ result : formatted ,
45
+ change : 'none'
46
+ }
47
+ }
48
+
49
+ // Get operative FormatThreshold pair...
50
+ let threshold : FormatThreshold
51
+ for (
52
+ let i = 0 , threshold = thresholds [ 0 ] ;
53
+ i < thresholds . length , n >= thresholds [ i ] . from ;
54
+ i ++
55
+ ) { }
56
+
57
+ // Build up units array to all units
58
+ // up to threshold.use
59
+ const units : QuantityAbbrSymbol [ ] = [ ]
60
+ for ( let i = 0 ; i < ABBR_SYMBOLS_ARRAY . length ; i ++ ) {
61
+ const current = ABBR_SYMBOLS_ARRAY [ i ]
62
+ units . push ( current )
63
+ if ( current === threshold ! . use ) {
64
+ break
65
+ }
66
+ }
67
+
68
+ const abbreviator = new Abbr ( units )
69
+
70
+ // Use thresholdFrom as a guide to how many chars are available
71
+ // first digit + comma = 2
72
+ // Possible trailing cents: '.xx'.length = 3
73
+ // 3 - 2 = 1
74
+ const charsAvail = usdFormatter . format ( threshold ! . from ) . length + 1
75
+ const abbr = abbreviator . abbreviate ( n , charsAvail ) // arbitrary, but good approx
76
+ const numStr = abbr . slice ( 0 , - 1 )
77
+ const abbreviation = abbr . slice ( - 1 )
78
+ const numerical = parseFloat ( numStr )
79
+
80
+ const integral = Math . floor ( numerical )
81
+ const integralString = usdFormatter . format ( integral )
82
+ const commas = integralString . split ( ',' ) . length - 1
83
+
84
+ // minus abbr, dec point, dollar sign, and roundingAdds / tilda,
85
+ // (1 + 1 + 1 + roundingAdds)
86
+ // ("precision" does NOT include the decimal point itself,
87
+ // so we have to explicitly factor it in.)
88
+ const roundedString = numerical . toPrecision ( charsAvail - commas - ( 3 + roundingAdds ) )
89
+ // remove trailing zeros, if any
90
+ const roundedNumerical = parseFloat ( roundedString )
91
+ const roundedIntegral = Math . trunc ( roundedNumerical )
92
+ const roundedIntegralString = usdFormatter . format ( roundedIntegral )
93
+
94
+ let decimalPortion = roundedNumerical - roundedIntegral
95
+ let result
96
+ if ( decimalPortion !== 0 ) {
97
+ // remove trailing zeros if any
98
+ decimalPortion = parseFloat ( decimalPortion . toFixed ( maxDecimal ) )
99
+ const decimalPortionString = decimalPortion . toString ( )
100
+ const afterDecimalString = decimalPortionString . slice ( decimalPortionString . indexOf ( '.' ) + 1 )
101
+ result = roundedIntegralString + '.' + afterDecimalString + abbreviation
102
+ }
103
+ else {
104
+ result = roundedIntegralString + abbreviation
105
+ }
106
+ // Did we lose any precision?
107
+ const rounded = ( roundedIntegral + decimalPortion !== n )
108
+ return {
109
+ full : formatted ,
110
+ result,
111
+ change : rounded ? 'rounded' : 'abbr'
112
+ }
113
+ }
114
+
115
+ export {
116
+ formatAndAbbreviateAsCurrency as default ,
117
+ type FormatThreshold ,
118
+ type QuantityAbbrSymbol
119
+ }
0 commit comments