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
@@ -1,48 +1,89 @@
1
1
  module Numerals::Conversions
2
2
 
3
3
  class <<self
4
- def [](type)
4
+ def [](type, options = nil)
5
5
  if type.respond_to?(:numerals_conversion)
6
- type.numerals_conversion
6
+ type.numerals_conversion(options || {})
7
7
  end
8
8
  end
9
9
 
10
10
  def order_of_magnitude(number, options={})
11
- self[number.class].order_of_magnitude(number, options)
11
+ self[number.class, options[:type_options]].order_of_magnitude(number, options)
12
12
  end
13
13
 
14
- # explicit: number_to_numeral x, mode, rounding
15
- # but for :free mode rounding is ignored except for the base
16
- # so if no rounding/rounding options are passed except for the base, the default mode should be :free
17
- # otherwise, the default mode should be :fixed...
18
-
19
- # number_to_numeral(x, precision: 3) = number_to_numeral(x, :fixed, Rounding[precision: 3])
20
- # number_to_numeral(x, precision: 3, base: 2) = number_to_numeral(x, :fixed, Rounding[precision: 3, base: 2])
21
- # number_to_numeral(x, :exact, base: 2) = number_to_numeral(x, :free, Rounding[:exact, base: 2])
22
-
23
- # Two ways of defining the conversion mode:
24
- # 1. fixed or free:
25
- # * :fixed means: adjust the number precision to the output rounding
26
- # * :free means: forget about the rounding, preserve the input number precision
27
- # But :free cheats: if rounding is not exact it really honors it by
28
- # converting the number to an exact numeral an then rounding it.
29
- # 2. exact or approximate
30
- # * :exact means: consider the number an exact quantity
31
- # * :approximate means: consider the number approximate; show only non-spurious digits.
32
- def number_to_numeral(number, *args)
33
- mode = extract_mode_from_args!(args)
34
- rounding = Rounding[*args]
35
- # mode ||= rounding.exact? ? :free : :fixed
36
- mode ||= :approximate
37
- if [:fixed, :free].include?(mode)
38
- mode = (mode == :fixed) == (rounding.exact?) ? :exact : :approximate
39
- end
40
- self[number.class].number_to_numeral(number, mode, rounding)
14
+ def number_of_digits(number, options={})
15
+ self[number.class, options[:type_options]].number_of_digits(number, options)
16
+ end
17
+
18
+ # Convert Numeral to Number
19
+ #
20
+ # read numeral, options={}
21
+ #
22
+ # If the input numeral is approximate and the destination type
23
+ # allows for arbitrary precision, then the destination context
24
+ # precision will be ignored and the precision of the input will be
25
+ # preserved. The :simplify option affects this case by generating
26
+ # only the mininimun number of digits needed.
27
+ #
28
+ # The :exact option will prevent this behaviour and always treat
29
+ # input as exact.
30
+ #
31
+ # Valid output options:
32
+ #
33
+ # * :type class of the output number
34
+ # * :context context (in the case of Flt::Num, Float) for the output
35
+ # * :simplify (for approximate input numeral/arbitrary precision type only)
36
+ # * :exact treat input numeral as if exact
37
+ #
38
+ def read(numeral, options={})
39
+ selector = options[:context] || options[:type]
40
+ exact_input = options[:exact]
41
+ approximate_simplified = options[:simplify]
42
+ conversions = self[selector, options[:type_options]]
43
+ conversions.read(numeral, exact_input, approximate_simplified)
44
+ end
45
+
46
+ # Convert Number to Numeral
47
+ #
48
+ # write number, options={}
49
+ #
50
+ # Valid options:
51
+ #
52
+ # * :rounding (a Rounding) (which defines output base as well)
53
+ # * :exact (exact input indicator)
54
+ #
55
+ # Approximate mode:
56
+ #
57
+ # If the input is treated as an approximation
58
+ # (which is the case for types such as Flt::Num, Float,...
59
+ # unless the :exact option is true) then no 'spurious' digits
60
+ # will be shown (digits that can take any value and the numeral
61
+ # still would convert to the original number if rounded to the same precision)
62
+ #
63
+ # In approximate mode, if rounding is simplifying? (:short), the shortest representation
64
+ # which rounds back to the origina number with the same precision is used.
65
+ # If rounding is :free and the output base is the same as the number
66
+ # internal radix, the exact precision (trailing zeros) of the number
67
+ # is represented.
68
+ #
69
+ # Exact mode:
70
+ #
71
+ # Is used for 'exact' types (such as Integer, Rational) or when the :exact
72
+ # option is defined to be true.
73
+ #
74
+ # The number is treated as an exact value, and converted according to
75
+ # Rounding. (in this case the :free and :short precision roundings are
76
+ # equivalent)
77
+ #
78
+ def write(number, options = {})
79
+ output_rounding = Rounding[options[:rounding] || Rounding[]]
80
+ conversion = self[number.class, options[:type_options]]
81
+ exact_input = conversion.exact?(number, options)
82
+ conversion.write(number, exact_input, output_rounding)
41
83
  end
42
84
 
43
- def numeral_to_number(numeral, type, *args)
44
- mode = extract_mode_from_args!(args) || :fixed
45
- self[type].numeral_to_number(numeral, mode, *args)
85
+ def exact?(number, options = {})
86
+ self[number.class, options[:type_options]].exact?(number, options)
46
87
  end
47
88
 
48
89
  private
@@ -2,8 +2,8 @@ require 'forwardable'
2
2
 
3
3
  module Numerals
4
4
 
5
- # Sequence of digit values type, with an Array-compatible interface
6
- # Having this encapsulated here allow to change the implementation
5
+ # Sequence of digit values, with an Array-compatible interface.
6
+ # Having this encapsulated here allows changing the implementation
7
7
  # e.g. to an Integer or packed in a String, ...
8
8
  class Digits
9
9
  def initialize(*args)
@@ -32,7 +32,7 @@ module Numerals
32
32
  :size, :map, :pop, :push, :shift, :unshift,
33
33
  :empty?, :first, :last, :any?, :all?, :[]=
34
34
 
35
- # The [] with a Range argument or two arguments (index, length)
35
+ # The [] operator with a Range argument or two arguments (index, length)
36
36
  # returns a Regular Array.
37
37
  def_delegators :@digits_array, :[], :replace
38
38
  include ModalSupport::StateEquivalent # maybe == with Arrays too?
@@ -68,7 +68,7 @@ module Numerals
68
68
 
69
69
  def zero?
70
70
  # value == 0
71
- @digits && @digits.all?{|d| d==0 }
71
+ !@digits_array || @digits_array.empty? || @digits_array.all?{|d| d==0 }
72
72
  end
73
73
 
74
74
  # Deep copy
@@ -93,7 +93,10 @@ module Numerals
93
93
  def truncate!(n)
94
94
  @digits_array.slice! n..-1
95
95
  end
96
+
97
+ def valid?
98
+ @digits_array.none? { |x| !x.kind_of?(Integer) || x < 0 || x >= @radix }
99
+ end
96
100
  end
97
101
 
98
102
  end
99
-
@@ -0,0 +1,160 @@
1
+ require 'forwardable'
2
+
3
+ module Numerals
4
+
5
+ # This converts the number representation contained in an ExpSetter
6
+ # scaling the base of the significand.
7
+ #
8
+ # This is typically used when the ExpSetter base is 2 to render the number
9
+ # in C99 '%A' format, i.e., in hexadecimal base. Only the significand is
10
+ # shown in base 16; the exponent is still a power of two, and represented
11
+ # in base 10.
12
+ #
13
+ # This is a generalization of the %A format where any base which is a
14
+ # power of the original base can be used for the significand.
15
+ #
16
+ # The number exponent is previously adjusted in the ExpSetter and that
17
+ # doesn't change, only the significand parts are converted from the
18
+ # original base `base` to the new base `base**base_scale`.
19
+ #
20
+ # This will require adjusting the repeating digits position and length,
21
+ # and adding leading 0s (in the original base) to the signficant
22
+ # and/or trailing digits may be required.
23
+ #
24
+ class Format::BaseScaler
25
+
26
+ def initialize(exp_setter, base_scale)
27
+ @setter = exp_setter
28
+ @numeral = @setter.numeral
29
+ @base_scale = base_scale
30
+ @scaled_base = @setter.base**@base_scale
31
+ adjust
32
+ end
33
+
34
+ include ModalSupport::BracketConstructor
35
+
36
+ attr_reader :base_scale, :scaled_base, :numeral
37
+
38
+ extend Forwardable
39
+ def_delegators :@setter,
40
+ :exponent_base, :exponent, :special?, :special,
41
+ :repeating?, :sign
42
+
43
+ def base
44
+ scaled_base
45
+ end
46
+
47
+ def fractional_part
48
+ ungrouped = @setter.fractional_part + (0...@scaling_trailing_size).map{|i| repeat_digit(i)}
49
+ grouped_digits ungrouped
50
+ end
51
+
52
+ def fractional_part_size
53
+ (@setter.fractional_part_size + @scaling_trailing_size)/@base_scale
54
+ end
55
+
56
+ def fractional_insignificant_size
57
+ if @setter.numeral.approximate?
58
+ (@setter.fractional_insignificant_size + @scaling_trailing_size)/@base_scale
59
+ else
60
+ 0
61
+ end
62
+ end
63
+
64
+ def integer_part
65
+ ungrouped = [0]*@scaling_leading_size + @setter.integer_part
66
+ grouped_digits ungrouped
67
+ end
68
+
69
+ def integer_part_size
70
+ (@setter.integer_part_size + @scaling_leading_size)/@base_scale
71
+ end
72
+
73
+ def integer_insignificant_size
74
+ if @setter.numeral.approximate?
75
+ (@setter.integer_insignificant_size + @scaling_leading_size)/@base_scale
76
+ else
77
+ 0
78
+ end
79
+ end
80
+
81
+ def repeat_size_size
82
+ @repeat_length/@base_scale
83
+ end
84
+
85
+ def repeat_insignificant_size
86
+ 0
87
+ end
88
+
89
+ def repeat_part
90
+ ungrouped = (@scaling_trailing_size...@scaling_trailing_size+@repeat_length).map{|i| repeat_digit(i)}
91
+ grouped_digits ungrouped
92
+ end
93
+
94
+ private
95
+
96
+ # Return the `scaled_base` digit corresponding to a group of `base_scale` `exponent_base` digits
97
+ def scaled_digit(group)
98
+ unless group.size == @base_scale
99
+ raise "Invalid digits group size for scaled_digit (is #{group.size}; should be #{@base_scale})"
100
+ end
101
+ v = 0
102
+ group.each do |digit|
103
+ v *= @setter.base
104
+ v += digit
105
+ end
106
+ v
107
+ end
108
+
109
+ # Convert base `exponent_base` digits to base `scaled_base` digits
110
+ # the number of digits must be a multiple of base_scale
111
+ def grouped_digits(digits)
112
+ unless (digits.size % @base_scale) == 0
113
+ raise "Invalid number of digits for group_digits (#{digits.size} is not a multiple of #{@base_scale})"
114
+ end
115
+ digits.each_slice(@base_scale).map{|group| scaled_digit(group)}
116
+ end
117
+
118
+ # Number of digits (base `exponent_base`) to be added to make the number
119
+ # of digits a multiple of `base_scale`.
120
+ def padding_size(digits_size)
121
+ (@base_scale - digits_size) % @base_scale
122
+ end
123
+
124
+ def adjust
125
+ return if special?
126
+ @setter_repeat_part = @setter.repeat_part
127
+ @setter_repeat_part_size = @setter.repeat_part_size
128
+
129
+ @scaling_trailing_size = padding_size(@setter.fractional_part_size)
130
+ @scaling_leading_size = padding_size(@setter.integer_part_size)
131
+
132
+ @repeat_length = @setter.repeat_part_size
133
+ while (@repeat_length % @base_scale) != 0
134
+ @repeat_length += @setter.repeat_part_size
135
+ end
136
+ end
137
+
138
+ def repeat_digit(i)
139
+ if @setter_repeat_part_size > 0
140
+ @setter_repeat_part[i % @setter_repeat_part_size]
141
+ else
142
+ 0
143
+ end
144
+ end
145
+
146
+ # Convert base digits to scaled base digits
147
+ def self.ugrouped_digits(digits, base, base_scale)
148
+ digits.flat_map { |d|
149
+ group = Numeral::Digits[base: base]
150
+ group.value = d
151
+ ungrouped = group.digits_array
152
+ if ungrouped.size < base_scale
153
+ ungrouped = [0]*(base_scale - ungrouped.size) + ungrouped
154
+ end
155
+ ungrouped
156
+ }
157
+ end
158
+ end
159
+
160
+ end
@@ -0,0 +1,218 @@
1
+ module Numerals
2
+
3
+ # Adjust exponent to be used in a Numeral expression;
4
+ # break up the numeral into integer, fractional and repeating parts.
5
+ #
6
+ # setter = ExpSetter[numeral]
7
+ # # To use 'fixed' format:
8
+ # setter.exponent = 0
9
+ # # To use scientific notation:
10
+ # setter.integer_part_size = 1
11
+ # # To adjust scientific notation to engineering mode:
12
+ # setter.integer_part_size += 1 (while setter.exponent % 3) != 0
13
+ # # To automatically choose between fixed/scientific format:
14
+ # setter.exponent = 0 # fixed
15
+ # if setter.leading_size > 6 || setter.trailing_size > 0
16
+ # setter.integer_part_size = 1 # scientific
17
+ # end
18
+ #
19
+ # # To access the numeric parts for formatting:
20
+ #  setter.sign
21
+ # setter.integer_part # digits before radix point
22
+ # setter.fractional_part # digits after radix point, before repetition
23
+ # setter.repeating_part # repeated digits
24
+ # setter.base # base for exponent
25
+ # setter.exponent # exponent
26
+ #
27
+ class Format::ExpSetter
28
+
29
+ def initialize(numeral, options={})
30
+ @insignificant_digits = options[:insignificant_digits] || 0
31
+ @numeral = numeral
32
+ @integer_part_size = @numeral.point
33
+ @digits = @numeral.digits
34
+ @exponent = 0
35
+ @repeat_size = @numeral.repeating? ? @digits.size - @numeral.repeat : 0
36
+ adjust
37
+ end
38
+
39
+ attr_reader :numeral
40
+
41
+ include ModalSupport::BracketConstructor
42
+ attr_reader :integer_part_size, :exponent
43
+ attr_reader :trailing_size, :leading_size
44
+
45
+ def base
46
+ @numeral.base
47
+ end
48
+
49
+ def exponent_base
50
+ base
51
+ end
52
+
53
+ def special?
54
+ @numeral.special?
55
+ end
56
+
57
+ def special
58
+ @numeral.special
59
+ end
60
+
61
+ def repeating?
62
+ @numeral.repeating?
63
+ end
64
+
65
+ def sign
66
+ @numeral.sign
67
+ end
68
+
69
+ def exponent=(v)
70
+ if @exponent != v
71
+ @integer_part_size -= (v - @exponent)
72
+ @exponent = v
73
+ adjust
74
+ end
75
+ end
76
+
77
+ def integer_part_size=(v)
78
+ if @integer_part_size != v
79
+ @exponent -= (v - @integer_part_size)
80
+ @integer_part_size = v
81
+ adjust
82
+ end
83
+ end
84
+
85
+ def repeat_part
86
+ if @numeral.repeating?
87
+ if @repeat_phase != 0 || @numeral.repeat < 0
88
+ start = @numeral.repeat + @repeat_phase
89
+ (start...start+repeat_part_size).map{|i| @numeral.digit_value_at(i)}
90
+ else
91
+ @digits[@numeral.repeat..-1]
92
+ end
93
+ else
94
+ []
95
+ end
96
+ end
97
+
98
+ def repeat_part_size
99
+ if @numeral.repeating?
100
+ @digits.size - @numeral.repeat
101
+ else
102
+ 0
103
+ end
104
+ end
105
+
106
+ def fractional_part
107
+ leading + @digits[@fractional_start...@fractional_end]
108
+ end
109
+
110
+ def integer_part
111
+ @digits[@integer_start...@integer_end] + trailing
112
+ end
113
+
114
+ def fractional_part_size
115
+ @fractional_end - @fractional_start + @leading_size
116
+ end
117
+
118
+ attr_reader :integer_insignificant_size, :fractional_insignificant_size
119
+
120
+ def repeat_insignificant_size
121
+ 0
122
+ end
123
+
124
+ private
125
+
126
+ def trailing
127
+ if @trailing_size > 0
128
+ (@digits.size...@digits.size+@trailing_size).map{|i| @numeral.digit_value_at(i)}
129
+ else
130
+ []
131
+ end
132
+ end
133
+
134
+ def leading
135
+ n = @leading_size
136
+ if @fractional_end < @fractional_start
137
+ n += @fractional_end - @fractional_start
138
+ end
139
+ if n > 0
140
+ [0]*n
141
+ else
142
+ []
143
+ end
144
+ end
145
+
146
+ def adjust
147
+ raise "Inconsistet number of insignficant digits" if @numeral.repeating? && @insignificant_digits > 0
148
+ if special?
149
+ @leading_size = @trailing_size = 0
150
+ @integer_start = @integer_end = 0
151
+ @fractional_start = @fractional_end = 0
152
+ @repeat_phase = 0
153
+ @integer_insignificant_size = @fractional_insignificant_size = 0
154
+ elsif @integer_part_size <= 0
155
+ @trailing_size = 0
156
+ # integer_part == []
157
+ @integer_start = @integer_end = @digits.size
158
+ @integer_insignificant_size = 0
159
+ if !@numeral.repeating? || @numeral.repeat >= 0 || @integer_part_size >= @numeral.repeat
160
+ @leading_size = -@integer_part_size
161
+ @fractional_start = 0
162
+ if @numeral.repeat
163
+ if @numeral.repeat >= 0
164
+ @fractional_end = @numeral.repeat
165
+ else
166
+ @fractional_end = @digits.size
167
+ end
168
+ @fractional_insignificant_size = 0
169
+ else
170
+ @fractional_end = @digits.size
171
+ @fractional_insignificant_size = [@insignificant_digits, @digits.size].min
172
+ end
173
+ @repeat_phase = 0
174
+ else
175
+ @leading_size = @numeral.repeat - @integer_part_size
176
+ @trailing_size = 0
177
+ @fractional_start = @fractional_end = @digits.size
178
+ @fractional_insignificant_size = 0
179
+ @repeat_phase = 0
180
+ end
181
+ elsif @integer_part_size >= @digits.size
182
+ @trailing_size = @integer_part_size - @digits.size
183
+ @leading_size = 0
184
+ @integer_start = 0
185
+ @integer_end = @digits.size
186
+ @integer_insignificant_size = @insignificant_digits
187
+ if @numeral.approximate?
188
+ @integer_insignificant_size += @trailing_size
189
+ end
190
+ @fractional_insignificant_size = 0
191
+ if @numeral.repeating?
192
+ @repeat_phase = @trailing_size % repeat_part_size
193
+ @fractional_start = @fractional_end = @digits.size
194
+ else
195
+ @repeat_phase = 0
196
+ @fractional_start = @fractional_end = @digits.size
197
+ end
198
+ else
199
+ @trailing_size = @leading_size = 0
200
+ @integer_start = 0
201
+ @integer_end = @integer_part_size
202
+ @integer_insignificant_size = [@insignificant_digits - (@digits.size - @integer_part_size), 0].max
203
+ if @numeral.repeating? && @numeral.repeat < @integer_part_size
204
+ @repeat_phase = (@integer_end - @numeral.repeat) % repeat_part_size
205
+ @fractional_start = @fractional_end = @digits.size
206
+ @fractional_insignificant_size = 0
207
+ else
208
+ @repeat_phase = 0
209
+ @fractional_start = @integer_part_size
210
+ @fractional_end = @numeral.repeat || @digits.size
211
+ @fractional_insignificant_size = [@insignificant_digits, @fractional_end - @fractional_start].min
212
+ end
213
+ end
214
+ end
215
+
216
+ end
217
+
218
+ end