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,477 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "money/currency/loader"
5
+ require "money/currency/heuristics"
6
+
7
+ class Money
8
+
9
+ # Represents a specific currency unit.
10
+ #
11
+ # @see https://en.wikipedia.org/wiki/Currency
12
+ # @see https://www.iso.org/iso-4217-currency-codes.html
13
+ class Currency
14
+ include Comparable
15
+ extend Enumerable
16
+ extend Money::Currency::Heuristics
17
+
18
+ # Keeping cached instances in sync between threads
19
+ @@mutex = Mutex.new
20
+ @@instances = {}
21
+
22
+ # Thrown when a Currency has been registered without all the attributes
23
+ # which are required for the current action.
24
+ class MissingAttributeError < StandardError
25
+ def initialize(method, currency, attribute)
26
+ super(
27
+ "Can't call Currency.#{method} - currency '#{currency}' is missing "\
28
+ "the attribute '#{attribute}'"
29
+ )
30
+ end
31
+ end
32
+
33
+ # Thrown when an unknown currency is requested.
34
+ class UnknownCurrency < ArgumentError; end
35
+
36
+ # Thrown when currency is not provided.
37
+ class NoCurrency < ArgumentError; end
38
+
39
+ class << self
40
+ def new(id)
41
+ id = id.to_s.downcase
42
+ unless stringified_keys.include?(id)
43
+ raise UnknownCurrency, "Unknown currency '#{id}'"
44
+ end
45
+
46
+ _instances[id] || @@mutex.synchronize { _instances[id] ||= super }
47
+ end
48
+
49
+ def _instances
50
+ @@instances
51
+ end
52
+
53
+ # Lookup a currency with given +id+ an returns a +Currency+ instance on
54
+ # success, +nil+ otherwise.
55
+ #
56
+ # @param [String, Symbol, #to_s] id Used to look into +table+ and
57
+ # retrieve the applicable attributes.
58
+ #
59
+ # @return [Money::Currency]
60
+ #
61
+ # @example
62
+ # Money::Currency.find(:eur) #=> #<Money::Currency id: eur ...>
63
+ # Money::Currency.find(:foo) #=> nil
64
+ def find(id)
65
+ id = id.to_s.downcase.to_sym
66
+ new(id)
67
+ rescue UnknownCurrency
68
+ nil
69
+ end
70
+
71
+ # Lookup a currency with given +num+ as an ISO 4217 numeric and returns an
72
+ # +Currency+ instance on success, +nil+ otherwise.
73
+ #
74
+ # @param [#to_s] num used to look into +table+ in +iso_numeric+ and find
75
+ # the right currency id.
76
+ #
77
+ # @return [Money::Currency]
78
+ #
79
+ # @example
80
+ # Money::Currency.find_by_iso_numeric(978) #=> #<Money::Currency id: eur ...>
81
+ # Money::Currency.find_by_iso_numeric(51) #=> #<Money::Currency id: amd ...>
82
+ # Money::Currency.find_by_iso_numeric('001') #=> nil
83
+ def find_by_iso_numeric(num)
84
+ num = num.to_s.rjust(3, '0')
85
+ return if num.empty?
86
+ id = iso_numeric_index[num]
87
+ new(id) if id
88
+ rescue UnknownCurrency
89
+ nil
90
+ end
91
+
92
+ # Wraps the object in a +Currency+ unless it's already a +Currency+
93
+ # object.
94
+ #
95
+ # @param [Object] object The object to attempt and wrap as a +Currency+
96
+ # object.
97
+ #
98
+ # @return [Money::Currency]
99
+ #
100
+ # @example
101
+ # c1 = Money::Currency.new(:usd)
102
+ # Money::Currency.wrap(nil) #=> nil
103
+ # Money::Currency.wrap(c1) #=> #<Money::Currency id: usd ...>
104
+ # Money::Currency.wrap("usd") #=> #<Money::Currency id: usd ...>
105
+ def wrap(object)
106
+ if object.nil?
107
+ nil
108
+ elsif object.is_a?(Currency)
109
+ object
110
+ else
111
+ Currency.new(object)
112
+ end
113
+ end
114
+
115
+ # List of known currencies.
116
+ #
117
+ # == monetary unit
118
+ # The standard unit of value of a currency, as the dollar in the United States or the peso in Mexico.
119
+ # https://www.answers.com/redirectSearch?query=monetary-unit
120
+ # == fractional monetary unit, subunit
121
+ # A monetary unit that is valued at a fraction (usually one hundredth) of the basic monetary unit
122
+ # https://www.answers.com/redirectSearch?query=fractional-monetary-unit-subunit
123
+ #
124
+ # See https://en.wikipedia.org/wiki/List_of_circulating_currencies and
125
+ # https://metacpan.org/release/TNGUYEN/Locale-Currency-Format-1.28/view/Format.pm
126
+ def table
127
+ @table ||= Loader.load_currencies
128
+ end
129
+
130
+ # List the currencies imported and registered
131
+ # @return [Array]
132
+ #
133
+ # @example
134
+ # Money::Currency.all()
135
+ # [#<Currency ..USD>, 'CAD', 'EUR']...
136
+ def all
137
+ table.keys.map do |curr|
138
+ c = Currency.new(curr)
139
+ if c.priority.nil?
140
+ raise MissingAttributeError.new(:all, c.id, :priority)
141
+ end
142
+ c
143
+ end.sort_by(&:priority)
144
+ end
145
+
146
+ # We need a string-based validator before creating an unbounded number of
147
+ # symbols.
148
+ # http://www.randomhacks.net.s3-website-us-east-1.amazonaws.com/2007/01/20/13-ways-of-looking-at-a-ruby-symbol/#11
149
+ # https://github.com/RubyMoney/money/issues/132
150
+ #
151
+ # @return [Set]
152
+ def stringified_keys
153
+ @stringified_keys ||= stringify_keys
154
+ end
155
+
156
+ # Register a new currency
157
+ #
158
+ # @param curr [Hash] information about the currency
159
+ # @option priority [Numeric] a numerical value you can use to sort/group
160
+ # the currency list
161
+ # @option iso_code [String] the international 3-letter code as defined
162
+ # by the ISO 4217 standard
163
+ # @option iso_numeric [Integer] the international 3-digit code as
164
+ # defined by the ISO 4217 standard
165
+ # @option name [String] the currency name
166
+ # @option symbol [String] the currency symbol (UTF-8 encoded)
167
+ # @option subunit [String] the name of the fractional monetary unit
168
+ # @option subunit_to_unit [Numeric] the proportion between the unit and
169
+ # the subunit
170
+ # @option separator [String] character between the whole and fraction
171
+ # amounts
172
+ # @option delimiter [String] character between each thousands place
173
+ def register(curr)
174
+ key = curr.fetch(:iso_code).downcase.to_sym
175
+ @@mutex.synchronize { _instances.delete(key.to_s) }
176
+ table[key] = curr
177
+ @stringified_keys = nil
178
+ clear_iso_numeric_cache
179
+ end
180
+
181
+ # Inherit a new currency from existing one
182
+ #
183
+ # @param parent_iso_code [String] the international 3-letter code as defined
184
+ # @param curr [Hash] See {register} method for hash structure
185
+ def inherit(parent_iso_code, curr)
186
+ parent_iso_code = parent_iso_code.downcase.to_sym
187
+ curr = table.fetch(parent_iso_code, {}).merge(curr)
188
+ register(curr)
189
+ end
190
+
191
+ # Unregister a currency.
192
+ #
193
+ # @param [Object] curr A Hash with the key `:iso_code`, or the ISO code
194
+ # as a String or Symbol.
195
+ #
196
+ # @return [Boolean] true if the currency previously existed, false
197
+ # if it didn't.
198
+ def unregister(curr)
199
+ if curr.is_a?(Hash)
200
+ key = curr.fetch(:iso_code).downcase.to_sym
201
+ else
202
+ key = curr.downcase.to_sym
203
+ end
204
+ existed = @table.delete(key)
205
+ @stringified_keys = nil if existed
206
+ existed ? true : false
207
+ end
208
+
209
+ def each
210
+ all.each { |c| yield(c) }
211
+ end
212
+
213
+ def reset!
214
+ @@instances = {}
215
+ @table = Loader.load_currencies
216
+ clear_iso_numeric_cache
217
+ end
218
+
219
+ private
220
+
221
+ def iso_numeric_index
222
+ @iso_numeric_index ||= table.each_with_object({}) do |(id, attrs), index|
223
+ index[attrs[:iso_numeric]] = id
224
+ end
225
+ end
226
+
227
+ def clear_iso_numeric_cache
228
+ @iso_numeric_index = nil
229
+ end
230
+
231
+ def stringify_keys
232
+ table.keys.each_with_object(Set.new) { |k, set| set.add(k.to_s.downcase) }
233
+ end
234
+ end
235
+
236
+ # @!attribute [r] id
237
+ # @return [Symbol] The symbol used to identify the currency, usually THE
238
+ # lowercase +iso_code+ attribute.
239
+ # @!attribute [r] priority
240
+ # @return [Integer] A numerical value you can use to sort/group the
241
+ # currency list.
242
+ # @!attribute [r] iso_code
243
+ # @return [String] The international 3-letter code as defined by the ISO
244
+ # 4217 standard.
245
+ # @!attribute [r] iso_numeric
246
+ # @return [String] The international 3-numeric code as defined by the ISO
247
+ # 4217 standard.
248
+ # @!attribute [r] name
249
+ # @return [String] The currency name.
250
+ # @!attribute [r] symbol
251
+ # @return [String] The currency symbol (UTF-8 encoded).
252
+ # @!attribute [r] disambiguate_symbol
253
+ # @return [String] Alternative currency used if symbol is ambiguous
254
+ # @!attribute [r] html_entity
255
+ # @return [String] The html entity for the currency symbol
256
+ # @!attribute [r] subunit
257
+ # @return [String] The name of the fractional monetary unit.
258
+ # @!attribute [r] subunit_to_unit
259
+ # @return [Integer] The proportion between the unit and the subunit
260
+ # @!attribute [r] decimal_mark
261
+ # @return [String] The decimal mark, or character used to separate the
262
+ # whole unit from the subunit.
263
+ # @!attribute [r] thousands_separator
264
+ # @return [String] The character used to separate thousands grouping of
265
+ # the whole unit.
266
+ # @!attribute [r] symbol_first
267
+ # @return [Boolean] Should the currency symbol precede the amount, or
268
+ # should it come after?
269
+ # @!attribute [r] smallest_denomination
270
+ # @return [Integer] Smallest amount of cash possible (in the subunit of
271
+ # this currency)
272
+
273
+ attr_reader :id, :priority, :iso_code, :iso_numeric, :name, :symbol,
274
+ :disambiguate_symbol, :html_entity, :subunit, :subunit_to_unit, :decimal_mark,
275
+ :thousands_separator, :symbol_first, :smallest_denomination, :format
276
+
277
+ alias_method :separator, :decimal_mark
278
+ alias_method :delimiter, :thousands_separator
279
+ alias_method :eql?, :==
280
+
281
+ # Create a new +Currency+ object.
282
+ #
283
+ # @param [String, Symbol, #to_s] id Used to look into +table+ and retrieve
284
+ # the applicable attributes.
285
+ #
286
+ # @return [Money::Currency]
287
+ #
288
+ # @example
289
+ # Money::Currency.new(:usd) #=> #<Money::Currency id: usd ...>
290
+ def initialize(id)
291
+ @id = id.to_sym
292
+ initialize_data!
293
+ end
294
+
295
+ # Compares +self+ with +other_currency+ against the value of +priority+
296
+ # attribute.
297
+ #
298
+ # @param [Money::Currency] other_currency The currency to compare to.
299
+ #
300
+ # @return [-1,0,1] -1 if less than, 0 is equal to, 1 if greater than
301
+ #
302
+ # @example
303
+ # c1 = Money::Currency.new(:usd)
304
+ # c2 = Money::Currency.new(:jpy)
305
+ # c1 <=> c2 #=> 1
306
+ # c2 <=> c1 #=> -1
307
+ # c1 <=> c1 #=> 0
308
+ def <=>(other_currency)
309
+ # <=> returns nil when one of the values is nil
310
+ comparison = self.priority <=> other_currency.priority || 0
311
+
312
+ if comparison == 0
313
+ self.id <=> other_currency.id
314
+ else
315
+ comparison
316
+ end
317
+ end
318
+
319
+ # Compares +self+ with +other_currency+ and returns +true+ if the are the
320
+ # same or if their +id+ attributes match.
321
+ #
322
+ # @param [Money::Currency] other_currency The currency to compare to.
323
+ #
324
+ # @return [Boolean]
325
+ #
326
+ # @example
327
+ # c1 = Money::Currency.new(:usd)
328
+ # c2 = Money::Currency.new(:jpy)
329
+ # c1 == c1 #=> true
330
+ # c1 == c2 #=> false
331
+ def ==(other_currency)
332
+ self.equal?(other_currency) || compare_ids(other_currency)
333
+ end
334
+
335
+ def compare_ids(other_currency)
336
+ other_currency_id = if other_currency.is_a?(Currency)
337
+ other_currency.id.to_s.downcase
338
+ else
339
+ other_currency.to_s.downcase
340
+ end
341
+ self.id.to_s.downcase == other_currency_id
342
+ end
343
+ private :compare_ids
344
+
345
+ # Returns a Integer hash value based on the +id+ attribute in order to use
346
+ # functions like & (intersection), group_by, etc.
347
+ #
348
+ # @return [Integer]
349
+ #
350
+ # @example
351
+ # Money::Currency.new(:usd).hash #=> 428936
352
+ def hash
353
+ id.hash
354
+ end
355
+
356
+ # Returns a human readable representation.
357
+ #
358
+ # @return [String]
359
+ #
360
+ # @example
361
+ # Money::Currency.new(:usd) #=> #<Currency id: usd ...>
362
+ def inspect
363
+ "#<#{self.class.name} id: #{id}, priority: #{priority}, symbol_first: #{symbol_first}, thousands_separator: #{thousands_separator}, html_entity: #{html_entity}, decimal_mark: #{decimal_mark}, name: #{name}, symbol: #{symbol}, subunit_to_unit: #{subunit_to_unit}, exponent: #{exponent}, iso_code: #{iso_code}, iso_numeric: #{iso_numeric}, subunit: #{subunit}, smallest_denomination: #{smallest_denomination}, format: #{format}>"
364
+ end
365
+
366
+ # Returns a string representation corresponding to the upcase +id+
367
+ # attribute.
368
+ #
369
+ # --
370
+ # DEV: id.to_s.upcase corresponds to iso_code but don't use ISO_CODE for consistency.
371
+ #
372
+ # @return [String]
373
+ #
374
+ # @example
375
+ # Money::Currency.new(:usd).to_s #=> "USD"
376
+ # Money::Currency.new(:eur).to_s #=> "EUR"
377
+ def to_s
378
+ id.to_s.upcase
379
+ end
380
+
381
+ # Returns a string representation corresponding to the upcase +id+
382
+ # attribute. Useful in cases where only implicit conversions are made.
383
+ #
384
+ # @return [String]
385
+ #
386
+ # @example
387
+ # Money::Currency.new(:usd).to_str #=> "USD"
388
+ # Money::Currency.new(:eur).to_str #=> "EUR"
389
+ def to_str
390
+ id.to_s.upcase
391
+ end
392
+
393
+ # Returns a symbol representation corresponding to the upcase +id+
394
+ # attribute.
395
+ #
396
+ # @return [Symbol]
397
+ #
398
+ # @example
399
+ # Money::Currency.new(:usd).to_sym #=> :USD
400
+ # Money::Currency.new(:eur).to_sym #=> :EUR
401
+ def to_sym
402
+ id.to_s.upcase.to_sym
403
+ end
404
+
405
+ # Conversion to +self+.
406
+ #
407
+ # @return [self]
408
+ def to_currency
409
+ self
410
+ end
411
+
412
+ # Returns currency symbol or iso code for currencies with no symbol.
413
+ #
414
+ # @return [String]
415
+ def code
416
+ symbol || iso_code
417
+ end
418
+
419
+ def symbol_first?
420
+ !!@symbol_first
421
+ end
422
+
423
+ # Returns true if a code currency is ISO.
424
+ #
425
+ # @return [Boolean]
426
+ #
427
+ # @example
428
+ # Money::Currency.new(:usd).iso?
429
+ #
430
+ def iso?
431
+ iso_numeric && iso_numeric != ''
432
+ end
433
+
434
+ # Returns true if a subunit is cents-based.
435
+ #
436
+ # @return [Boolean]
437
+ #
438
+ # @example
439
+ # Money::Currency.new(:usd).cents_based?
440
+ #
441
+ def cents_based?
442
+ subunit_to_unit == 100
443
+ end
444
+
445
+ # Returns the relation between subunit and unit as a base 10 exponent.
446
+ #
447
+ # Note that MGA and MRU are exceptions and are rounded to 1
448
+ # @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
449
+ #
450
+ # @return [Integer]
451
+ def exponent
452
+ Math.log10(subunit_to_unit).round
453
+ end
454
+ alias decimal_places exponent
455
+
456
+ private
457
+
458
+ def initialize_data!
459
+ data = self.class.table[@id]
460
+ @alternate_symbols = data[:alternate_symbols]
461
+ @decimal_mark = data[:decimal_mark]
462
+ @disambiguate_symbol = data[:disambiguate_symbol]
463
+ @html_entity = data[:html_entity]
464
+ @iso_code = data[:iso_code]
465
+ @iso_numeric = data[:iso_numeric]
466
+ @name = data[:name]
467
+ @priority = data[:priority]
468
+ @smallest_denomination = data[:smallest_denomination]
469
+ @subunit = data[:subunit]
470
+ @subunit_to_unit = data[:subunit_to_unit]
471
+ @symbol = data[:symbol]
472
+ @symbol_first = data[:symbol_first]
473
+ @thousands_separator = data[:thousands_separator]
474
+ @format = data[:format]
475
+ end
476
+ end
477
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'money/locale_backend/errors'
4
+
5
+ class Money
6
+ module LocaleBackend
7
+ class Base; end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'money/locale_backend/base'
4
+
5
+ class Money
6
+ module LocaleBackend
7
+ class Currency < Base
8
+ def lookup(key, currency)
9
+ currency.public_send(key) if currency.respond_to?(key)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Money
4
+ module LocaleBackend
5
+ class NotSupported < StandardError; end
6
+ class Unknown < ArgumentError; end
7
+ end
8
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'money/locale_backend/base'
4
+
5
+ class Money
6
+ module LocaleBackend
7
+ class I18n < Base
8
+ KEY_MAP = {
9
+ thousands_separator: :delimiter,
10
+ decimal_mark: :separator,
11
+ symbol: :unit,
12
+ format: :format,
13
+ }.freeze
14
+
15
+ def initialize
16
+ raise NotSupported, 'I18n not found' unless defined?(::I18n)
17
+ end
18
+
19
+ def lookup(key, _)
20
+ i18n_key = KEY_MAP[key]
21
+
22
+ ::I18n.t i18n_key, scope: 'number.currency.format', raise: true
23
+ rescue ::I18n::MissingTranslationData
24
+ ::I18n.t i18n_key, scope: 'number.format', default: nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Money
4
+ class Allocation
5
+ # Allocates a specified amount into parts based on their proportions or distributes
6
+ # it evenly when the number of parts is specified numerically.
7
+ #
8
+ # The total of the allocated amounts will always equal the original amount.
9
+ #
10
+ # The parts can be specified as:
11
+ # Numeric — performs the split between a given number of parties evenly
12
+ # Array<Numeric> — allocates the amounts proportionally to the given array
13
+ #
14
+ # @param amount [Numeric] The total amount to be allocated.
15
+ # @param parts [Numeric, Array<Numeric>] Number of parts to split into or an array (proportions for allocation)
16
+ # @param decimal_cutoff [Boolean, Integer] Controls per-split precision:
17
+ # - When true, splits are truncated to whole amounts.
18
+ # - When an Integer N, splits are rounded to N decimal places.
19
+ # - When false or nil, no rounding/truncation is applied.
20
+ # Defaults to true (whole amounts).
21
+ #
22
+ # @return [Array<Numeric>] An array containing the allocated amounts.
23
+ # @raise [ArgumentError] If parts is empty or not provided.
24
+ def self.generate(amount, parts, decimal_cutoff = true)
25
+ parts = if parts.is_a?(Numeric)
26
+ Array.new(parts, 1)
27
+ elsif parts.all?(&:zero?)
28
+ Array.new(parts.count, 1)
29
+ else
30
+ parts.dup
31
+ end
32
+
33
+ raise ArgumentError, 'need at least one part' if parts.empty?
34
+
35
+ if [amount, *parts].any? { |i| i.is_a?(BigDecimal) || i.is_a?(Float) || i.is_a?(Rational) }
36
+ amount = convert_to_big_decimal(amount)
37
+ parts.map! { |p| convert_to_big_decimal(p) }
38
+ end
39
+
40
+ result = []
41
+ remaining_amount = amount
42
+ round_to_whole = decimal_cutoff.is_a?(TrueClass)
43
+ round_to_precision = decimal_cutoff.is_a?(Integer)
44
+
45
+ parts_sum = parts.sum
46
+
47
+ until parts.empty? do
48
+ part = parts.pop
49
+
50
+ current_split = 0
51
+ if parts_sum > 0
52
+ current_split = remaining_amount * part / parts_sum
53
+ current_split =
54
+ if round_to_whole
55
+ current_split.truncate
56
+ elsif round_to_precision
57
+ current_split.round(decimal_cutoff)
58
+ else
59
+ current_split
60
+ end
61
+ end
62
+
63
+ result.unshift current_split
64
+ remaining_amount -= current_split
65
+ parts_sum -= part
66
+ end
67
+
68
+ result
69
+ end
70
+
71
+ # Converts a given number to BigDecimal.
72
+ # This method supports inputs of BigDecimal, Rational, and other numeric types by ensuring they are all returned
73
+ # as BigDecimal instances for consistent handling.
74
+ #
75
+ # @param number [Numeric, BigDecimal, Rational] The number to convert.
76
+ # @return [BigDecimal] The converted number as a BigDecimal.
77
+ def self.convert_to_big_decimal(number)
78
+ if number.is_a? BigDecimal
79
+ number
80
+ elsif number.is_a? Rational
81
+ BigDecimal(number.to_f.to_s)
82
+ else
83
+ BigDecimal(number.to_s)
84
+ end
85
+ end
86
+ end
87
+ end