numerals 0.0.0 → 0.1.0

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +149 -5
  3. data/lib/numerals/conversions/bigdecimal.rb +209 -9
  4. data/lib/numerals/conversions/context_conversion.rb +40 -0
  5. data/lib/numerals/conversions/float.rb +106 -71
  6. data/lib/numerals/conversions/flt.rb +115 -44
  7. data/lib/numerals/conversions/integer.rb +32 -3
  8. data/lib/numerals/conversions/rational.rb +27 -3
  9. data/lib/numerals/conversions.rb +74 -33
  10. data/lib/numerals/digits.rb +8 -5
  11. data/lib/numerals/format/base_scaler.rb +160 -0
  12. data/lib/numerals/format/exp_setter.rb +218 -0
  13. data/lib/numerals/format/format.rb +257 -0
  14. data/lib/numerals/format/input.rb +140 -0
  15. data/lib/numerals/format/mode.rb +157 -0
  16. data/lib/numerals/format/notation.rb +51 -0
  17. data/lib/numerals/format/notations/html.rb +53 -0
  18. data/lib/numerals/format/notations/latex.rb +48 -0
  19. data/lib/numerals/format/notations/text.rb +141 -0
  20. data/lib/numerals/format/output.rb +167 -0
  21. data/lib/numerals/format/symbols.rb +565 -0
  22. data/lib/numerals/format/text_parts.rb +35 -0
  23. data/lib/numerals/format.rb +25 -0
  24. data/lib/numerals/formatting_aspect.rb +36 -0
  25. data/lib/numerals/numeral.rb +34 -21
  26. data/lib/numerals/repeat_detector.rb +99 -0
  27. data/lib/numerals/rounding.rb +340 -181
  28. data/lib/numerals/version.rb +1 -1
  29. data/lib/numerals.rb +4 -2
  30. data/numerals.gemspec +1 -1
  31. data/test/test_base_scaler.rb +189 -0
  32. data/test/test_big_conversions.rb +105 -0
  33. data/test/test_digits_definition.rb +23 -28
  34. data/test/test_exp_setter.rb +732 -0
  35. data/test/test_float_conversions.rb +48 -30
  36. data/test/test_flt_conversions.rb +476 -80
  37. data/test/test_format.rb +124 -0
  38. data/test/test_format_input.rb +226 -0
  39. data/test/test_format_mode.rb +124 -0
  40. data/test/test_format_output.rb +789 -0
  41. data/test/test_integer_conversions.rb +22 -22
  42. data/test/test_numeral.rb +35 -0
  43. data/test/test_rational_conversions.rb +28 -28
  44. data/test/test_repeat_detector.rb +72 -0
  45. data/test/test_rounding.rb +158 -0
  46. data/test/test_symbols.rb +32 -0
  47. metadata +38 -5
  48. data/lib/numerals/formatting/digits_definition.rb +0 -75
@@ -0,0 +1,565 @@
1
+ module Numerals
2
+
3
+ #
4
+ # * insignificant_digit : symbol to represent insignificant digits;
5
+ # use nil (the default) to omit insignificant digits and 0
6
+ # for a zero digit. Insignificant digits are digits which, in an
7
+ # approximate value, are not determined: they could change to any
8
+ # other digit and the approximated value would be the same.
9
+ #
10
+ # * repeating : (boolean) support repeating decimals?
11
+ #
12
+ class Format::Symbols < FormattingAspect
13
+
14
+ class Digits < FormattingAspect
15
+
16
+ DEFAULT_DIGITS = %w(0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)
17
+
18
+ def initialize(*args)
19
+ @digits = DEFAULT_DIGITS
20
+ @downcase_digits = @digits.map(&:downcase)
21
+ @max_base = @digits.size
22
+ @case_sensitive = false
23
+ @uppercase = false
24
+ @lowercase = false
25
+ set! *args
26
+ end
27
+
28
+ include ModalSupport::StateEquivalent
29
+
30
+ set do |*args|
31
+ options = extract_options(*args)
32
+ options.each do |option, value|
33
+ send :"#{option}=", value
34
+ end
35
+ end
36
+
37
+ attr_reader :digits_string, :max_base, :case_sensitive, :uppercase, :lowercase
38
+ attr_writer :case_sensitive
39
+
40
+ def digits(options = {})
41
+ base = options[:base] || @max_base
42
+ if base >= @max_base
43
+ @digits
44
+ else
45
+ @digits[0, base]
46
+ end
47
+ end
48
+
49
+ def digits=(digits)
50
+ if digits.is_a?(String)
51
+ @digits = digits.each_char.to_a
52
+ else
53
+ @digits = digits
54
+ end
55
+ @max_base = @digits.size
56
+ @lowercase = @digits.all? { |d| d.downcase == d }
57
+ @uppercase = @digits.all? { |d| d.upcase == d }
58
+ @downcase_digits = @digits.map(&:downcase)
59
+ if @digits.uniq.size != @max_base
60
+ raise "Inconsistent digits"
61
+ end
62
+ end
63
+
64
+ def uppercase=(v)
65
+ @uppercase = v
66
+ self.digits = @digits.map(&:upcase) if v
67
+ end
68
+
69
+ def lowercase=(v)
70
+ @lowercase = v
71
+ self.digits = @digits.map(&:downcase) if v
72
+ end
73
+
74
+ def case_sensitive?
75
+ case_sensitive
76
+ end
77
+
78
+ def is_digit?(digit_symbol, options={})
79
+ base = options[:base] || @max_base
80
+ raise "Invalid base" if base > @max_base
81
+ v = digit_value(digit_symbol)
82
+ v && v < base
83
+ end
84
+
85
+ def digit_value(digit)
86
+ if @case_sensitive
87
+ @digits.index(digit)
88
+ else
89
+ @downcase_digits.index(digit.downcase)
90
+ end
91
+ end
92
+
93
+ def digit_symbol(v, options={})
94
+ base = options[:base] || @max_base
95
+ raise "Invalid base" if base > @max_base
96
+ v >= 0 && v < base ? @digits[v] : nil
97
+ end
98
+
99
+ # Convert sequence of digits to its text representation.
100
+ # The nil value can be used in the digits sequence to
101
+ # represent the group separator.
102
+ def digits_text(digit_values, options={})
103
+ insignificant_digits = options[:insignificant_digits] || 0
104
+ num_digits = digit_values.reduce(0) { |num, digit|
105
+ digit.nil? ? num : num + 1
106
+ }
107
+ num_digits -= insignificant_digits
108
+ digit_values.map { |d|
109
+ if d.nil?
110
+ options[:separator]
111
+ else
112
+ num_digits -= 1
113
+ if num_digits >= 0
114
+ digit_symbol(d, options)
115
+ else
116
+ options[:insignificant_symbol]
117
+ end
118
+ end
119
+ }.join
120
+ end
121
+
122
+ def parameters
123
+ params = {}
124
+ params[:digits] = @digits
125
+ params[:case_sensitive] = @case_sensitive
126
+ params[:uppercase] = @uppercase
127
+ params[:lowercase] = @lowercase
128
+ params
129
+ end
130
+
131
+ def to_s
132
+ # TODO: show only non-defaults
133
+ "Digits[#{parameters.inspect.unwrap('{}')}]"
134
+ end
135
+
136
+ def inspect
137
+ "Format::Symbols::#{self}"
138
+ end
139
+
140
+ def dup
141
+ Digits[parameters]
142
+ end
143
+
144
+ private
145
+
146
+ def extract_options(*args)
147
+ options = {}
148
+ args = args.first if args.size == 1 && args.first.kind_of?(Array)
149
+ args.each do |arg|
150
+ case arg
151
+ when Hash
152
+ options.merge! arg
153
+ when String, Array
154
+ options[:digits] = arg
155
+ when Format::Symbols::Digits
156
+ options.merge! arg.parameters
157
+ else
158
+ raise "Invalid Symbols::Digits definition"
159
+ end
160
+ end
161
+ options
162
+ end
163
+
164
+ end
165
+
166
+ DEFAULTS = {
167
+ nan: 'NaN',
168
+ infinity: 'Infinity',
169
+ plus: '+',
170
+ minus: '-',
171
+ exponent: 'e',
172
+ point: '.',
173
+ group_separator: ',',
174
+ zero: nil,
175
+ repeat_begin: '<',
176
+ repeat_end: '>',
177
+ repeat_suffix: '...',
178
+ #repeat_detect: false,
179
+ show_plus: false,
180
+ show_exponent_plus: false,
181
+ uppercase: false,
182
+ lowercase: false,
183
+ show_zero: true,
184
+ show_point: false,
185
+ repeat_delimited: false,
186
+ repeat_count: 3,
187
+ grouping: [],
188
+ insignificant_digit: nil,
189
+ repeating: true
190
+ }
191
+
192
+ def initialize(*args)
193
+ DEFAULTS.each do |param, value|
194
+ instance_variable_set "@#{param}", value
195
+ end
196
+
197
+ # @digits is a mutable Object, so we don't want
198
+ # to set it from DEFAULTS (which would share the
199
+ # default Digits among all Symbols)
200
+ @digits = Format::Symbols::Digits[]
201
+
202
+ # TODO: justification/padding
203
+ # width, adjust_mode (left, right, internal), fill_symbol
204
+
205
+ # TODO: base_suffixes, base_preffixes, show_base
206
+
207
+ set! *args
208
+ end
209
+
210
+ # TODO: transmit uppercase/lowercase to digits
211
+
212
+ attr_reader :digits, :nan, :infinity, :plus, :minus, :exponent, :point,
213
+ :group_separator, :zero, :insignificant_digit
214
+ attr_reader :repeat_begin, :repeat_end, :repeat_suffix, :repeat_delimited
215
+ attr_reader :show_plus, :show_exponent_plus, :uppercase, :lowercase,
216
+ :show_zero, :show_point
217
+ attr_reader :grouping, :repeat_count, :repeating
218
+
219
+ attr_writer :uppercase, :lowercase, :nan, :infinity, :plus,
220
+ :minus, :exponent, :point, :group_separator, :zero,
221
+ :repeat_begin, :repeat_end, :repeat_suffix,
222
+ :show_plus, :show_exponent_plus, :show_zero, :show_point,
223
+ :repeat_delimited, :repeat_count, :grouping,
224
+ :insignificant_digit, :repeating
225
+
226
+ include ModalSupport::StateEquivalent
227
+
228
+ def positive_infinity
229
+ txt = ""
230
+ txt << @plus if @show_plus
231
+ txt << @infinity
232
+ txt
233
+ end
234
+
235
+ def negative_infinity
236
+ txt = ""
237
+ txt << @minus
238
+ txt << @infinity
239
+ txt
240
+ end
241
+
242
+ def zero
243
+ if @zero
244
+ @zero
245
+ else
246
+ @digits.digit_symbol(0)
247
+ end
248
+ end
249
+
250
+ def grouping?
251
+ !@grouping.empty? && @group_separator && !@group_separator.empty?
252
+ end
253
+
254
+ set do |*args|
255
+ options = extract_options(*args)
256
+ options.each do |option, value|
257
+ if option == :digits
258
+ @digits.set! value
259
+ else
260
+ send :"#{option}=", value
261
+ end
262
+ end
263
+ apply_case!
264
+ end
265
+
266
+ attr_writer :digits, :nan, :infinity,
267
+ :plus, :minus, :exponent, :point, :group_separator, :zero,
268
+ :repeat_begin, :repeat_end, :repeat_suffix, :show_plus,
269
+ :show_exponent_plus, :uppercase, :show_zero, :show_point,
270
+ :grouping, :repeat_count
271
+
272
+ aspect :repeat do |*args|
273
+ # TODO accept hash :begin, :end, :suffix, ...
274
+ end
275
+
276
+ aspect :grouping do |*args|
277
+ args.each do |arg|
278
+ case arg
279
+ when Symbol
280
+ if arg == :thousands
281
+ @groups = [3]
282
+ end
283
+ when String
284
+ @group_separator = arg
285
+ when Array
286
+ @groups = groups
287
+ end
288
+ end
289
+ end
290
+
291
+ def case_sensitive
292
+ @digits.case_sensitive
293
+ end
294
+
295
+ def case_sensitive?
296
+ @digits.case_sensitive
297
+ end
298
+
299
+ def case_sensitive=(v)
300
+ @digits.set! case_sensitive: v
301
+ end
302
+
303
+ aspect :group_thousands do |sep = nil|
304
+ @group_separator = sep if sep
305
+ @grouping = [3]
306
+ end
307
+
308
+ aspect :signs do |plus, minus|
309
+ @plus = plus
310
+ @minus = minus
311
+ end
312
+
313
+ aspect :plus do |plus, which = nil|
314
+ case plus
315
+ when nil, false
316
+ case which
317
+ when :exponent, :exp
318
+ @show_exponent_plus = false
319
+ when :both, :all
320
+ @show_plus = @show_exponent_plus = false
321
+ else
322
+ @show_plus = false
323
+ end
324
+ when true
325
+ case which
326
+ when :exponent, :exp
327
+ @show_exponent_plus = true
328
+ when :both, :all
329
+ @show_plus = @show_exponent_plus = true
330
+ else
331
+ @show_plus = true
332
+ end
333
+ when :both, :all
334
+ @show_plus = @show_exponent_plus = true
335
+ when :exponent, :exp
336
+ @show_exponent_plus = true
337
+ else
338
+ @plus = plus
339
+ case which
340
+ when :exponent, :exp
341
+ @show_exponent_plus = true
342
+ when :both, :all
343
+ @show_plus = @show_exponent_plus = true
344
+ else
345
+ @show_plus = true
346
+ end
347
+ end
348
+ end
349
+
350
+ aspect :minus do |minus|
351
+ @minus = minus
352
+ end
353
+
354
+ def parameters(abbreviated=false)
355
+ params = {}
356
+ DEFAULTS.each do |param, default|
357
+ value = instance_variable_get("@#{param}")
358
+ if !abbreviated || value != default
359
+ params[param] = value
360
+ end
361
+ end
362
+ if !abbreviated || @digits != Format::Symbols::Digits[]
363
+ params[:digits] = @digits
364
+ end
365
+ params
366
+ end
367
+
368
+ def to_s
369
+ "Digits[#{parameters(true).inspect.unwrap('{}')}]"
370
+ end
371
+
372
+ def inspect
373
+ "Format::Symbols::#{self}"
374
+ end
375
+
376
+ def dup
377
+ Format::Symbols[parameters]
378
+ end
379
+
380
+ # Group digits (inserting nil values as separators)
381
+ def group_digits(digits)
382
+ if grouping?
383
+ grouped = []
384
+ i = 0
385
+ while digits.size > 0
386
+ l = @grouping[i]
387
+ l = digits.size if l > digits.size
388
+ grouped = [nil] + grouped if grouped.size > 0
389
+ grouped = digits[-l, l] + grouped
390
+ digits = digits[0, digits.length - l]
391
+ i += 1 if i < @grouping.size - 1
392
+ end
393
+ grouped
394
+ else
395
+ digits
396
+ end
397
+ end
398
+
399
+ def digits_text(digit_values, options={})
400
+ if options[:with_grouping]
401
+ digit_values = group_digits(digit_values)
402
+ end
403
+ insignificant_symbol = @insignificant_digit
404
+ insignificant_symbol = zero if insignificant_symbol == 0
405
+ @digits.digits_text(
406
+ digit_values,
407
+ options.merge(
408
+ separator: @group_separator,
409
+ insignificant_symbol: insignificant_symbol
410
+ )
411
+ )
412
+ end
413
+
414
+ # Generate a regular expression to match any of the passed symbols.
415
+ #
416
+ # symbols.regexp(:nan, :plus, :minus) #=> "(NaN|+|-)"
417
+ #
418
+ # The special symbol :digits can also be passed to generate all the digits,
419
+ # in which case the :base option can be used to generate digits
420
+ # only for some base smaller than the maximum defined for digits.
421
+ #
422
+ # symbols.regexp(:digits, :point, base: 10) # => "(0|1|...|9)"
423
+ #
424
+ # The option :no_capture can be used to avoid generating a capturing
425
+ # group; otherwise the result is captured group (surrounded by parenthesis)
426
+ #
427
+ # symbols.regexp(:digits, no_capture: true) # => "(?:...)"
428
+ #
429
+ # The :case_sensitivity option is used to generate a regular expression
430
+ # that matches the case of the text as defined by ghe case_sensitive
431
+ # attribute of the Symbols. If this option is used the result should not be
432
+ # used in a case-insensitive regular expression (/.../i).
433
+ #
434
+ # /#{symbols.regexp(:digits, case_sensitivity: true)}/
435
+ #
436
+ # If the options is not used, than the regular expression should be
437
+ # be made case-insensitive according to the Symbols:
438
+ #
439
+ # if symbols.case_sensitive?
440
+ # /#{symbols.regexp(:digits)}/
441
+ # else
442
+ # /#{symbols.regexp(:digits)}/i
443
+ #
444
+ def regexp(*args)
445
+ options = args.pop if args.last.is_a?(Hash)
446
+ options ||= {}
447
+ symbols = args
448
+ digits = symbols.delete(:digits)
449
+ grouped_digits = symbols.delete(:grouped_digits)
450
+ symbols = symbols.map { |s| send(s.to_sym) }
451
+ if grouped_digits
452
+ symbols += [group_separator, insignificant_digit]
453
+ elsif digits
454
+ symbols += [insignificant_digit]
455
+ end
456
+ if digits || grouped_digits
457
+ symbols += @digits.digits(options)
458
+ end
459
+ regexp_group(symbols, options)
460
+ end
461
+
462
+ def digits_values(digits_text, options = {})
463
+ digit_pattern = Regexp.new(
464
+ regexp(
465
+ :grouped_digits,
466
+ options.merge(no_capture: true)
467
+ ),
468
+ !case_sensitive? ? Regexp::IGNORECASE : 0
469
+ )
470
+ digits_text.scan(digit_pattern).map { |digit|
471
+ case digit
472
+ when /\A#{regexp(:insignificant_digit, case_sensitivity: true)}\Z/
473
+ 0
474
+ when /\A#{regexp(:group_separator, case_sensitivity: true)}\Z/
475
+ nil
476
+ else
477
+ @digits.digit_value(digit)
478
+ end
479
+ }.compact
480
+ end
481
+
482
+ private
483
+
484
+ def regexp_char(c, options = {})
485
+ c_upcase = c.upcase
486
+ c_downcase = c.downcase
487
+ if c_downcase != c_upcase && !case_sensitive? && options[:case_sensitivity]
488
+ "(?:#{Regexp.escape(c_upcase)}|#{Regexp.escape(c_downcase)})"
489
+ else
490
+ Regexp.escape(c)
491
+ end
492
+ end
493
+
494
+ def regexp_symbol(symbol, options = {})
495
+ symbol.each_char.map { |c| regexp_char(c, options) }.join
496
+ end
497
+
498
+ def regexp_group(symbols, options = {})
499
+ capture = !options[:no_capture]
500
+ symbols = Array(symbols).compact.select { |s| !s.empty? }
501
+ .map{ |d| regexp_symbol(d, options) }.join('|')
502
+ if capture
503
+ "(#{symbols})"
504
+ else
505
+ "(?:#{symbols})"
506
+ end
507
+ end
508
+
509
+ def extract_options(*args)
510
+ options = {}
511
+ args = args.first if args.size == 1 && args.first.kind_of?(Array)
512
+ args.each do |arg|
513
+ case arg
514
+ when Hash
515
+ options.merge! arg
516
+ when Format::Symbols::Digits
517
+ options[:digits] = arg
518
+ when Format::Symbols
519
+ options.merge! arg.parameters
520
+ when :group_thousands
521
+ options[:grouping] = [3]
522
+ when :case_sensitive
523
+ options[:case_sensitive] = true
524
+ else
525
+ raise "Invalid Symbols definition"
526
+ end
527
+ end
528
+ options
529
+ end
530
+
531
+ def apply_case!
532
+ if @uppercase
533
+ @nan = @nan.upcase
534
+ @infinity = @infinity.upcase
535
+ @plus = @plus.upcase
536
+ @exponent = @exponent.upcase
537
+ @point = @point.upcase
538
+ @group_separator = @group_separator.upcase
539
+ @zero = @zero.upcase if @zero
540
+ @repeat_begin = @repeat_begin.upcase
541
+ @repeat_end = @repeat_end.upcase
542
+ @repeat_suffix = @repeat_suffix.upcase
543
+ @digits = @digits[uppercase: true]
544
+ elsif @lowercase
545
+ @nan = @nan.downcase
546
+ @infinity = @infinity.downcase
547
+ @plus = @plus.downcase
548
+ @exponent = @exponent.downcase
549
+ @point = @point.downcase
550
+ @group_separator = @group_separator.downcase
551
+ @zero = @zero.downcase if @zero
552
+ @repeat_begin = @repeat_begin.downcase
553
+ @repeat_end = @repeat_end.downcase
554
+ @repeat_suffix = @repeat_suffix.downcase
555
+ @digits = @digits[lowercase: true]
556
+ end
557
+ end
558
+
559
+ def cased(symbol)
560
+ @uppercase ? symbol.upcase : @lowercase ? symbol.downcase : symbol
561
+ end
562
+
563
+ end
564
+
565
+ end
@@ -0,0 +1,35 @@
1
+
2
+ # Numeral parts represented in text form
3
+ class TextParts
4
+
5
+ def self.text_part(*names)
6
+ names.each do |name|
7
+ attr_writer name.to_sym
8
+ define_method name do
9
+ instance_variable_get("@#{name}") || ""
10
+ end
11
+ define_method :"#{name}?" do
12
+ !send(name.to_sym).empty?
13
+ end
14
+ end
15
+ end
16
+
17
+ def initialize(numeral = nil)
18
+ @numeral = numeral
19
+ @special = nil
20
+ @sign = @integer = @fractional = @repeat = @exponent = @exponent_base = nil
21
+ @integer_value = @exponent_value = @exponent_base_value = nil
22
+ @detect_repeat = false
23
+ end
24
+
25
+ text_part :special
26
+ text_part :sign, :integer, :fractional, :repeat, :exponent, :exponent_base
27
+
28
+ attr_accessor :integer_value, :exponent_value, :exponent_base_value, :detect_repeat
29
+ attr_reader :numeral
30
+
31
+ def detect_repeat?
32
+ @detect_repeat
33
+ end
34
+
35
+ end
@@ -0,0 +1,25 @@
1
+ module Numerals
2
+ class Format < FormattingAspect
3
+ class Error < StandardError
4
+ end
5
+
6
+ class InvalidRepeatingNumeral < Error
7
+ end
8
+
9
+ class InvalidNumberFormat < Error
10
+ end
11
+
12
+ class InvalidNumericType < Error
13
+ end
14
+ end
15
+ end
16
+
17
+ require 'numerals/format/mode'
18
+ require 'numerals/format/symbols'
19
+ require 'numerals/format/exp_setter'
20
+ require 'numerals/format/base_scaler'
21
+ require 'numerals/format/text_parts'
22
+ require 'numerals/format/notation'
23
+ require 'numerals/format/output'
24
+ require 'numerals/format/input'
25
+ require 'numerals/format/format'
@@ -0,0 +1,36 @@
1
+ module Numerals
2
+
3
+ class FormattingAspect
4
+
5
+ def [](*args)
6
+ set *args
7
+ end
8
+
9
+ def self.[](*args)
10
+ new *args
11
+ end
12
+
13
+ def set(*args)
14
+ dup.set! *args
15
+ end
16
+
17
+ def self.aspect(aspect, &blk)
18
+ define_method :"set_#{aspect}!" do |*args|
19
+ instance_exec(*args, &blk)
20
+ self
21
+ end
22
+ define_method :"set_#{aspect}" do |*args|
23
+ dup.send(:"set_#{aspect}!", *args)
24
+ end
25
+ end
26
+
27
+ def self.set(*args, &blk)
28
+ define_method :"set!" do |*args|
29
+ instance_exec(*args, &blk)
30
+ self
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end