-
Notifications
You must be signed in to change notification settings - Fork 21
/
short_formatter.ex
308 lines (250 loc) · 10.4 KB
/
short_formatter.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
defmodule Cldr.Number.Formatter.Short do
@moduledoc """
Formats a number according to the locale-specific `:short` formats
This is best explained by some
examples:
iex> Cldr.Number.to_string 123, TestBackend.Cldr, format: :short
{:ok, "123"}
iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short
{:ok, "1K"}
iex> Cldr.Number.to_string 523456789, TestBackend.Cldr, format: :short
{:ok, "523M"}
iex> Cldr.Number.to_string 7234567890, TestBackend.Cldr, format: :short
{:ok, "7B"}
iex> Cldr.Number.to_string 7234567890, TestBackend.Cldr, format: :long
{:ok, "7 billion"}
These formats are compact representations however they do lose
precision in the presentation in favour of human readability.
Note that for a `:currency` short format the number of decimal places
is retrieved from the currency definition itself. You can see the difference
in the following examples:
iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short, currency: "EUR"
{:ok, "€1K"}
iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short, currency: "EUR", fractional_digits: 2
{:ok, "€1.23K"}
iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short, currency: "JPY"
{:ok, "¥1K"}
**This module is not part of the public API and is subject
to change at any time.**
"""
alias Cldr.Math
alias Cldr.Number.{System, Format, Formatter}
alias Cldr.Locale
alias Cldr.LanguageTag
alias Cldr.Number.Format.Options
# Notes from Unicode TR35 on formatting short formats:
#
# To format a number N, the greatest type less than or equal to N is
# used, with the appropriate plural category. N is divided by the type, after
# removing the number of zeros in the pattern, less 1. APIs supporting this
# format should provide control over the number of significant or fraction
# digits.
#
# If the value is precisely 0, or if the type is less than 1000, then the
# normal number format pattern for that sort of object is supplied. For
# example, formatting 1200 would result in “$1.2K”, while 990 would result in
# simply “$990”.
#
# Thus N=12345 matches <pattern type="10000" count="other">00 K</pattern> . N
# is divided by 1000 (obtained from 10000 after removing "00" and restoring one
# "0". The result is formatted according to the normal decimal pattern. With no
# fractional digits, that yields "12 K".
@spec to_string(Math.number_or_decimal(), atom(), Cldr.backend(), Options.t()) ::
{:ok, String.t()} | {:error, {module(), String.t()}}
def to_string(number, _style, _backend, _options) when is_binary(number) do
{:error,
{
ArgumentError,
"Not a number: #{inspect number}. Long and short formats only support number or Decimal arguments"
}
}
end
def to_string(number, style, backend, options) do
locale = options.locale || backend.default_locale()
with {:ok, locale} <- Cldr.validate_locale(locale, backend),
{:ok, number_system} <- System.system_name_from(options.number_system, locale, backend) do
short_format_string(number, style, locale, number_system, backend, options)
end
end
@spec short_format_string(
Math.number_or_decimal(),
atom,
Locale.locale_name() | LanguageTag.t(),
System.system_name(),
Cldr.backend(),
Options.t()
) :: {:ok, String.t()} | {:error, {module(), String.t()}}
defp short_format_string(number, style, locale, number_system, backend, options) do
format_rules =
locale
|> Format.formats_for!(number_system, backend)
|> Map.fetch!(style)
{normalized_number, format} = choose_short_format(number, format_rules, options, backend)
options = digits(options, options.fractional_digits)
format = Options.maybe_adjust_currency_symbol(format, options.currency_symbol)
Formatter.Decimal.to_string(normalized_number, format, backend, options)
end
@doc """
Returns the exponent that will be applied
when formatting the given number as a short
format.
This function is primarily intended to support
pluralization for compact numbers (numbers
formatted with the `format: :short` option) since
some languages pluralize compact numbers differently
to a fully expressed number.
Such rules are defined for the locale "fr" from
CLDR version 38 with the intention that additional
rules will be added in later versions.
## Examples
iex> Cldr.Number.Formatter.Short.short_format_exponent 1234
{1000, 1}
iex> Cldr.Number.Formatter.Short.short_format_exponent 12345
{10000, 2}
iex> Cldr.Number.Formatter.Short.short_format_exponent 123456789
{100000000, 3}
iex> Cldr.Number.Formatter.Short.short_format_exponent 123456789, locale: "th"
{100000000, 3}
"""
def short_format_exponent(number, options \\ []) when is_list(options) do
with {locale, backend} = Cldr.locale_and_backend_from(options),
number_system = Keyword.get(options, :number_system, :default),
{:ok, number_system} <- System.system_name_from(number_system, locale, backend),
{:ok, all_formats} <- Format.formats_for(locale, number_system, backend) do
formats = Map.fetch!(all_formats, :decimal_short)
pluralizer = Module.concat(backend, Number.Cardinal)
options =
options
|> Map.new
|> Map.put_new(:locale, locale)
|> Map.put_new(:number_system, number_system)
|> Map.put_new(:currency, nil)
case get_short_format_rule(number, formats, options, backend) do
[range, plural_selectors] ->
normalized_number = normalise_number(number, range, plural_selectors.other)
plural_key = pluralization_key(normalized_number, options)
[_format, number_of_zeros] = pluralizer.pluralize(plural_key, options.locale, plural_selectors)
{range, number_of_zeros}
{number, _format} ->
{number, 0}
end
end
end
# For short formats the fractional digits should be 0 unless otherwise specified,
# even for currencies
defp digits(options, nil) do
Map.put(options, :fractional_digits, 0)
end
defp digits(options, _digits) do
options
end
defp choose_short_format(number, format_rules, options, backend)
when is_number(number) and number < 0 do
{number, format} = choose_short_format(abs(number), format_rules, options, backend)
{number * -1, format}
end
defp choose_short_format(%Decimal{sign: -1 = sign} = number, format_rules, options, backend) do
{normalized_number, format} =
choose_short_format(Decimal.abs(number), format_rules, options, backend)
{Decimal.mult(normalized_number, sign), format}
end
defp choose_short_format(number, format_rules, options, backend) do
pluralizer = Module.concat(backend, Number.Cardinal)
case get_short_format_rule(number, format_rules, options, backend) do
# Its a short format
[range, plural_selectors] ->
normalized_number = normalise_number(number, range, plural_selectors.other)
plural_key = pluralization_key(normalized_number, options)
[format, _number_of_zeros] = pluralizer.pluralize(plural_key, options.locale, plural_selectors)
{normalized_number, format}
# Its a standard format
{number, format} ->
{number, format}
end
end
defp get_short_format_rule(number, _format_rules, options, backend) when is_number(number) and number < 1000 do
format =
options.locale
|> Format.formats_for!(options.number_system, backend)
|> Map.get(standard_or_currency(options))
{number, format}
end
defp get_short_format_rule(number, format_rules, options, backend) when is_number(number) do
format_rules
|> Enum.filter(fn [range, _rules] -> range <= number end)
|> Enum.reverse()
|> hd
|> maybe_get_default_format(number, options, backend)
end
defp get_short_format_rule(%Decimal{} = number, format_rules, options, backend) do
rule =
number
|> Decimal.round(0, :floor)
|> Decimal.to_integer()
|> get_short_format_rule(format_rules, options, backend)
case rule do
{_ignore, format} -> {number, format}
rule -> rule
end
end
defp maybe_get_default_format([_range, %{other: ["0", _]}], number, options, backend) do
{_, format} = get_short_format_rule(0, [], options, backend)
{number, format}
end
defp maybe_get_default_format(rule, _number, _options, _backend) do
rule
end
defp standard_or_currency(options) do
if options.currency do
:currency
else
:standard
end
end
@one_thousand Decimal.new(1000)
defp normalise_number(%Decimal{} = number, range, number_of_zeros) do
if Cldr.Decimal.compare(number, @one_thousand) == :lt do
number
else
Decimal.div(number, Decimal.new(adjustment(range, number_of_zeros)))
end
end
defp normalise_number(number, _range, _number_of_zeros) when number < 1000 do
number
end
defp normalise_number(number, _range, ["0", _number_of_zeros]) do
number
end
defp normalise_number(number, range, [_format, number_of_zeros]) do
number / adjustment(range, number_of_zeros)
end
# TODO: We can precompute these at compile time which would
# save this lookup
defp adjustment(range, number_of_zeros) when is_integer(number_of_zeros) do
(range / Math.power_of_10(number_of_zeros - 1))
|> trunc
end
defp adjustment(range, [_, number_of_zeros]) when is_integer(number_of_zeros) do
adjustment(range, number_of_zeros)
end
# The pluralization key has to consider when there is an
# exact match and when the number would be rounded up. When
# rounded up it also has to not be an exact match.
defp pluralization_key(number, options) do
rounding_mode = Map.get_lazy(options, :rounding_mode, &Cldr.Math.default_rounding_mode/0)
if (rounded = Cldr.Math.round(number, 0, rounding_mode)) <= number do
# Rounded number <= number means that the
# pluralization key is the same integer part
# so no issue
number
else
# The rounded number is greater than the normalized
# number so the plural key is different but not exactly
# equal so we add an offset so pluralization works
# correctly (we don't want to trigger an exact match;
# although this relies on exact matches always being integers
# which as of CLDR39 they are).
rounded + 0.1
end
end
end