numerals 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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