tiny-sharp-rb 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'money/money/formatting_rules'
4
+
5
+ class Money
6
+ class Formatter
7
+ DEFAULTS = {
8
+ thousands_separator: '',
9
+ decimal_mark: '.'
10
+ }.freeze
11
+
12
+ # Creates a formatted price string according to several rules.
13
+ #
14
+ # @param [Hash] rules The options used to format the string.
15
+ #
16
+ # @return [String]
17
+ #
18
+ # @option rules [Boolean, String] :display_free (false) Whether a zero
19
+ # amount of money should be formatted of "free" or as the supplied string.
20
+ #
21
+ # @example
22
+ # Money.us_dollar(0).format(display_free: true) #=> "free"
23
+ # Money.us_dollar(0).format(display_free: "gratis") #=> "gratis"
24
+ # Money.us_dollar(0).format #=> "$0.00"
25
+ #
26
+ # @option rules [Boolean] :with_currency (false) Whether the currency name
27
+ # should be appended to the result string.
28
+ #
29
+ # @example
30
+ # Money.ca_dollar(100).format #=> "$1.00"
31
+ # Money.ca_dollar(100).format(with_currency: true) #=> "$1.00 CAD"
32
+ # Money.us_dollar(85).format(with_currency: true) #=> "$0.85 USD"
33
+ #
34
+ # @option rules [Boolean] :rounded_infinite_precision (false) Whether the
35
+ # amount of money should be rounded when using {default_infinite_precision}
36
+ #
37
+ # @example
38
+ # Money.us_dollar(100.1).format #=> "$1.001"
39
+ # Money.us_dollar(100.1).format(rounded_infinite_precision: true) #=> "$1"
40
+ # Money.us_dollar(100.9).format(rounded_infinite_precision: true) #=> "$1.01"
41
+ #
42
+ # @option rules [Boolean] :no_cents (false) Whether cents should be omitted.
43
+ #
44
+ # @example
45
+ # Money.ca_dollar(100).format(no_cents: true) #=> "$1"
46
+ # Money.ca_dollar(599).format(no_cents: true) #=> "$5"
47
+ #
48
+ # @option rules [Boolean] :no_cents_if_whole (false) Whether cents should be
49
+ # omitted if the cent value is zero
50
+ #
51
+ # @example
52
+ # Money.ca_dollar(10000).format(no_cents_if_whole: true) #=> "$100"
53
+ # Money.ca_dollar(10034).format(no_cents_if_whole: true) #=> "$100.34"
54
+ #
55
+ # @option rules [Boolean, String, nil] :symbol (true) Whether a money symbol
56
+ # should be prepended to the result string. The default is true. This method
57
+ # attempts to pick a symbol that's suitable for the given currency.
58
+ #
59
+ # @example
60
+ # Money.new(100, "USD") #=> "$1.00"
61
+ # Money.new(100, "GBP") #=> "£1.00"
62
+ # Money.new(100, "EUR") #=> "€1.00"
63
+ #
64
+ # # Same thing.
65
+ # Money.new(100, "USD").format(symbol: true) #=> "$1.00"
66
+ # Money.new(100, "GBP").format(symbol: true) #=> "£1.00"
67
+ # Money.new(100, "EUR").format(symbol: true) #=> "€1.00"
68
+ #
69
+ # # You can specify a false expression or an empty string to disable
70
+ # # prepending a money symbol.§
71
+ # Money.new(100, "USD").format(symbol: false) #=> "1.00"
72
+ # Money.new(100, "GBP").format(symbol: nil) #=> "1.00"
73
+ # Money.new(100, "EUR").format(symbol: "") #=> "1.00"
74
+ #
75
+ # # If the symbol for the given currency isn't known, then it will default
76
+ # # to "¤" as symbol.
77
+ # Money.new(100, "AWG").format(symbol: true) #=> "¤1.00"
78
+ #
79
+ # # You can specify a string as value to enforce using a particular symbol.
80
+ # Money.new(100, "AWG").format(symbol: "ƒ") #=> "ƒ1.00"
81
+ #
82
+ # # You can specify a indian currency format
83
+ # Money.new(10000000, "INR").format(south_asian_number_formatting: true) #=> "1,00,000.00"
84
+ # Money.new(10000000).format(south_asian_number_formatting: true) #=> "$1,00,000.00"
85
+ #
86
+ # @option rules [Boolean, String, nil] :decimal_mark (true) Whether the
87
+ # currency should be separated by the specified character or '.'
88
+ #
89
+ # @example
90
+ # # If a string is specified, it's value is used.
91
+ # Money.new(100, "USD").format(decimal_mark: ",") #=> "$1,00"
92
+ #
93
+ # # If the decimal_mark for a given currency isn't known, then it will default
94
+ # # to "." as decimal_mark.
95
+ # Money.new(100, "FOO").format #=> "$1.00"
96
+ #
97
+ # @option rules [Boolean, String, nil] :thousands_separator (true) Whether
98
+ # the currency should be delimited by the specified character or ','
99
+ #
100
+ # @example
101
+ # # If a falsey value is specified, no thousands_separator is used.
102
+ # Money.new(100000, "USD").format(thousands_separator: false) #=> "1000.00"
103
+ # Money.new(100000, "USD").format(thousands_separator: nil) #=> "1000.00"
104
+ # Money.new(100000, "USD").format(thousands_separator: "") #=> "1000.00"
105
+ #
106
+ # # If true is specified, the locale or default thousands_separator is used.
107
+ # Money.new(100000, "USD").format(thousands_separator: true) #=> "1,000.00"
108
+ #
109
+ # # If a string is specified, it's value is used.
110
+ # Money.new(100000, "USD").format(thousands_separator: ".") #=> "$1.000.00"
111
+ #
112
+ # # If the thousands_separator for a given currency isn't known, then it will
113
+ # # default to "," as thousands_separator.
114
+ # Money.new(100000, "FOO").format #=> "$1,000.00"
115
+ #
116
+ # @option rules [Boolean] :html_wrap (false) Whether all currency parts should be HTML-formatted.
117
+ #
118
+ # @example
119
+ # Money.ca_dollar(570).format(html_wrap: true, with_currency: true)
120
+ # #=> "<span class=\"money-currency-symbol\">$</span><span class=\"money-whole\">5</span><span class=\"money-decimal-mark\">.</span><span class=\"money-decimal\">70</span> <span class=\"money-currency\">CAD</span>"
121
+ #
122
+ # @option rules [Boolean] :sign_before_symbol (false) Whether the sign should be
123
+ # before the currency symbol.
124
+ #
125
+ # @example
126
+ # # You can specify to display the sign before the symbol for negative numbers
127
+ # Money.new(-100, "GBP").format(sign_before_symbol: true) #=> "-£1.00"
128
+ # Money.new(-100, "GBP").format(sign_before_symbol: false) #=> "£-1.00"
129
+ # Money.new(-100, "GBP").format #=> "£-1.00"
130
+ #
131
+ # @option rules [Boolean] :sign_positive (false) Whether positive numbers should be
132
+ # signed, too.
133
+ #
134
+ # @example
135
+ # # You can specify to display the sign with positive numbers
136
+ # Money.new(100, "GBP").format(sign_positive: true, sign_before_symbol: true) #=> "+£1.00"
137
+ # Money.new(100, "GBP").format(sign_positive: true, sign_before_symbol: false) #=> "£+1.00"
138
+ # Money.new(100, "GBP").format(sign_positive: false, sign_before_symbol: true) #=> "£1.00"
139
+ # Money.new(100, "GBP").format(sign_positive: false, sign_before_symbol: false) #=> "£1.00"
140
+ # Money.new(100, "GBP").format #=> "£+1.00"
141
+ #
142
+ # @option rules [Boolean] :disambiguate (false) Prevents the result from being ambiguous
143
+ # due to equal symbols for different currencies. Uses the `disambiguate_symbol`.
144
+ #
145
+ # @example
146
+ # Money.new(10000, "USD").format(disambiguate: false) #=> "$100.00"
147
+ # Money.new(10000, "CAD").format(disambiguate: false) #=> "$100.00"
148
+ # Money.new(10000, "USD").format(disambiguate: true) #=> "$100.00"
149
+ # Money.new(10000, "CAD").format(disambiguate: true) #=> "C$100.00"
150
+ #
151
+ # @option rules [Boolean] :translate (true) `true` Checks for custom
152
+ # symbol definitions using I18n.
153
+ #
154
+ # @example
155
+ # # With the following entry in the translation files:
156
+ # # en:
157
+ # # number:
158
+ # # currency:
159
+ # # symbol:
160
+ # # CAD: "CAD$"
161
+ # Money.new(10000, "CAD").format(translate: true) #=> "CAD$100.00"
162
+ #
163
+ # @option rules [Boolean] :drop_trailing_zeros (false) Ignore trailing zeros after
164
+ # the decimal mark
165
+ #
166
+ # @example
167
+ # Money.new(89000, :btc).format(drop_trailing_zeros: true) #=> B⃦0.00089
168
+ # Money.new(110, :usd).format(drop_trailing_zeros: true) #=> $1.1
169
+ #
170
+ # @option rules [Boolean] :delimiter_pattern (/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/) Regular expression to set the placement
171
+ # for the thousands delimiter
172
+ #
173
+ # @example
174
+ # Money.new(89000, :btc).format(delimiter_pattern: /(\d)(?=\d)/) #=> B⃦8,9,0.00
175
+ #
176
+ # @option rules [String] :format (nil) Provide a template for formatting. `%u` will be replaced
177
+ # with the symbol (if present) and `%n` will be replaced with the number.
178
+ #
179
+ # @example
180
+ # Money.new(10000, "USD").format(format: '%u %n') #=> "$ 100.00"
181
+ # Money.new(10000, "USD").format(format: '<span>%u%n</span>') #=> "<span>$100.00</span>"
182
+ #
183
+ # Note that the default rules can be defined through {Money.default_formatting_rules} hash.
184
+ #
185
+ # @see Money.default_formatting_rules Money.default_formatting_rules for more information.
186
+ def initialize(money, *rules)
187
+ @money = money
188
+ @currency = money.currency
189
+ @rules = FormattingRules.new(@currency, *rules)
190
+ end
191
+
192
+ def to_s
193
+ return free_text if show_free_text?
194
+ result = format_number
195
+ formatted = append_sign(result)
196
+ append_currency_symbol(formatted)
197
+ end
198
+
199
+ def thousands_separator
200
+ val = lookup :thousands_separator
201
+
202
+ return val unless val == true
203
+
204
+ lookup_default :thousands_separator
205
+ end
206
+
207
+ def decimal_mark
208
+ lookup :decimal_mark
209
+ end
210
+
211
+ alias_method :delimiter, :thousands_separator
212
+ alias_method :separator, :decimal_mark
213
+
214
+ private
215
+
216
+ attr_reader :money, :currency, :rules
217
+
218
+ def format_number
219
+ whole_part, decimal_part = extract_whole_and_decimal_parts
220
+
221
+ # Format whole and decimal parts separately
222
+ decimal_part = format_decimal_part(decimal_part)
223
+ whole_part = format_whole_part(whole_part)
224
+
225
+ # Assemble the final formatted amount
226
+ if rules[:html_wrap]
227
+ if decimal_part.nil?
228
+ html_wrap(whole_part, "whole")
229
+ else
230
+ [
231
+ html_wrap(whole_part, "whole"),
232
+ html_wrap(decimal_mark, "decimal-mark"),
233
+ html_wrap(decimal_part, "decimal")
234
+ ].join
235
+ end
236
+ else
237
+ [whole_part, decimal_part].compact.join(decimal_mark)
238
+ end
239
+ end
240
+
241
+ def append_sign(formatted_number)
242
+ sign = money.negative? ? '-' : ''
243
+
244
+ if rules[:sign_positive] == true && money.positive?
245
+ sign = '+'
246
+ end
247
+
248
+ if rules[:sign_before_symbol] == true
249
+ sign_before = sign
250
+ sign = ''
251
+ end
252
+
253
+ symbol_value = symbol_value_from(rules)
254
+
255
+ if symbol_value && !symbol_value.empty?
256
+ if rules[:html_wrap]
257
+ symbol_value = html_wrap(symbol_value, "currency-symbol")
258
+ end
259
+
260
+ rules[:format]
261
+ .gsub('%u', [sign_before, symbol_value].join)
262
+ .gsub('%n', [sign, formatted_number].join)
263
+ else
264
+ formatted_number = "#{sign_before}#{sign}#{formatted_number}"
265
+ end
266
+ end
267
+
268
+ def append_currency_symbol(formatted_number)
269
+ if rules[:with_currency]
270
+ currency_part =
271
+ if rules[:html_wrap]
272
+ html_wrap(currency.to_s, "currency")
273
+ else
274
+ currency.to_s
275
+ end
276
+
277
+ "#{formatted_number} #{currency_part}"
278
+ else
279
+ formatted_number
280
+ end
281
+ end
282
+
283
+ def show_free_text?
284
+ money.zero? && rules[:display_free]
285
+ end
286
+
287
+ def html_wrap(string, class_name)
288
+ "<span class=\"money-#{class_name}\">#{string}</span>"
289
+ end
290
+
291
+ def free_text
292
+ rules[:display_free].respond_to?(:to_str) ? rules[:display_free] : 'free'
293
+ end
294
+
295
+ def format_whole_part(value)
296
+ # Apply thousands_separator
297
+ value.gsub(rules[:delimiter_pattern]) do |digit_to_delimit|
298
+ "#{digit_to_delimit}#{thousands_separator}"
299
+ end
300
+ end
301
+
302
+ def extract_whole_and_decimal_parts
303
+ fractional = money.fractional.abs
304
+
305
+ # Round the infinite precision part if needed
306
+ fractional = fractional.round if rules[:rounded_infinite_precision]
307
+
308
+ # Translate subunits into units
309
+ fractional_units = BigDecimal(fractional) / currency.subunit_to_unit
310
+
311
+ # Split the result and return whole and decimal parts separately
312
+ fractional_units.to_s('F').split('.')
313
+ end
314
+
315
+ def format_decimal_part(value)
316
+ return nil if currency.decimal_places == 0 && !Money.default_infinite_precision
317
+ return nil if rules[:no_cents]
318
+ return nil if rules[:no_cents_if_whole] && value.to_i == 0
319
+
320
+ # Pad value, making up for missing zeroes at the end
321
+ value = value.ljust(currency.decimal_places, '0')
322
+
323
+ # Drop trailing zeros if needed
324
+ value.gsub!(/0*$/, '') if rules[:drop_trailing_zeros]
325
+
326
+ value.empty? ? nil : value
327
+ end
328
+
329
+ def lookup(key)
330
+ return rules[key] || DEFAULTS[key] if rules.has_key?(key)
331
+
332
+ lookup_default key
333
+ end
334
+
335
+ def lookup_default(key)
336
+ (Money.locale_backend && Money.locale_backend.lookup(key, currency)) || DEFAULTS[key]
337
+ end
338
+
339
+ def symbol_value_from(rules)
340
+ if rules.has_key?(:symbol)
341
+ if rules[:symbol] === true
342
+ if rules[:disambiguate] && currency.disambiguate_symbol
343
+ currency.disambiguate_symbol
344
+ else
345
+ money.symbol
346
+ end
347
+ elsif rules[:symbol]
348
+ rules[:symbol]
349
+ else
350
+ ""
351
+ end
352
+ elsif rules[:html_wrap]
353
+ currency.html_entity == '' ? currency.symbol : currency.html_entity
354
+ elsif rules[:disambiguate] && currency.disambiguate_symbol
355
+ currency.disambiguate_symbol
356
+ else
357
+ money.symbol
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Money
4
+ class FormattingRules
5
+ def initialize(currency, *raw_rules)
6
+ @currency = currency
7
+
8
+ # support for old format parameters
9
+ @rules = normalize_formatting_rules(raw_rules)
10
+
11
+ @rules = default_formatting_rules.merge(@rules) unless @rules[:ignore_defaults]
12
+ @rules = translate_formatting_rules(@rules) if @rules[:translate]
13
+ @rules[:format] ||= determine_format
14
+ @rules[:delimiter_pattern] ||= delimiter_pattern_rule(@rules)
15
+ end
16
+
17
+ def [](key)
18
+ @rules[key]
19
+ end
20
+
21
+ def has_key?(key)
22
+ @rules.has_key? key
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :currency
28
+
29
+ # Cleans up formatting rules.
30
+ #
31
+ # @param [Hash] rules
32
+ #
33
+ # @return [Hash]
34
+ def normalize_formatting_rules(rules)
35
+ if rules.size == 0
36
+ rules = {}
37
+ elsif rules.size == 1
38
+ rules = rules.pop
39
+ rules = rules.dup if rules.is_a?(Hash)
40
+
41
+ if rules.is_a?(Symbol)
42
+ warn '[DEPRECATION] Use Hash when passing rules to Money#format.'
43
+ rules = { rules => true }
44
+ end
45
+ end
46
+
47
+ if !rules.include?(:decimal_mark) && rules.include?(:separator)
48
+ rules[:decimal_mark] = rules[:separator]
49
+ end
50
+
51
+ if !rules.include?(:thousands_separator) && rules.include?(:delimiter)
52
+ rules[:thousands_separator] = rules[:delimiter]
53
+ end
54
+
55
+ rules
56
+ end
57
+
58
+ def default_formatting_rules
59
+ Money.default_formatting_rules || {}
60
+ end
61
+
62
+ def translate_formatting_rules(rules)
63
+ begin
64
+ rules[:symbol] = I18n.t currency.iso_code, scope: "number.currency.symbol", raise: true
65
+ rescue I18n::MissingTranslationData
66
+ # Do nothing
67
+ end
68
+ rules
69
+ end
70
+
71
+ def determine_format
72
+ Money.locale_backend&.lookup(:format, @currency) || default_format
73
+ end
74
+
75
+ def default_format
76
+ if currency.format
77
+ currency.format
78
+ else
79
+ currency.symbol_first? ? "%u%n" : "%n %u"
80
+ end
81
+ end
82
+
83
+ def delimiter_pattern_rule(rules)
84
+ if rules[:south_asian_number_formatting]
85
+ # from https://blog.revathskumar.com/2014/11/regex-comma-seperated-indian-currency-format.html
86
+ /(\d+?)(?=(\d\d)+(\d)(?!\d))(\.\d+)?/
87
+ else
88
+ /(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'money/locale_backend/errors'
4
+ require 'money/locale_backend/i18n'
5
+ require 'money/locale_backend/currency'
6
+
7
+ class Money
8
+ module LocaleBackend
9
+ BACKENDS = {
10
+ i18n: Money::LocaleBackend::I18n,
11
+ currency: Money::LocaleBackend::Currency
12
+ }.freeze
13
+
14
+ def self.find(name)
15
+ raise Unknown, "Unknown locale backend: #{name}" unless BACKENDS.key?(name)
16
+
17
+ BACKENDS[name].new
18
+ end
19
+ end
20
+ end