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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 20e32ed4a377f88140d75db5dce2070dceca3b30
4
- data.tar.gz: 835f3416de372cf7fa26f2607b74176b666d88b5
3
+ metadata.gz: 0e7931e88c73e54a2d95c47f45d222562dc03a02
4
+ data.tar.gz: 0179555b287f177e458d854708929838c9619bcc
5
5
  SHA512:
6
- metadata.gz: a97323fa538d4e1fe2cc9c57b5fc9bc2e09dcf67260b8a74828be88415d7672efb6f314eefa6e23f41542d739bc291cf0c3f36c8b446eaf82c359b8beb0a7e7a
7
- data.tar.gz: e894386209dbfbc2e3471a00cf866a39e320a99a00e8f6c467dfa4c3947a87eac6797409036efe4c6446041dbb9d8ad4a1dfeb83a7d8a3ae5522932d5a81d40d
6
+ metadata.gz: fbdfea7324a61b2d25888c0553acefac0911e97ed9ddd5e9217064694f1f66fefea0ba968941cead3634e0b9c5a4bfa55e12cfcb16896c42407f057724446580
7
+ data.tar.gz: ea99d01ee36c91692c926d64f022e08274f89b0a3f4f4eb96f0a4dcaaac1c4340c1c6484d9a1b29f235ae370a2fce903624b1e5656f3297ee1026728cf4d6b87
data/README.md CHANGED
@@ -1,9 +1,146 @@
1
- numerals
1
+ Numerals
2
2
  ========
3
3
 
4
- Number representation as text.
4
+ The Numerals module provides formatted input/output for numeric types.
5
5
 
6
- This will be a successor gem to Nio.
6
+ ## Use
7
+
8
+ require 'numerals'
9
+ include Numerals
10
+
11
+ The Numeral class is used internally to hold the representation of a numeric
12
+ quantity as numeral in a positional system. Since repeating figures are
13
+ supported in Numeral, a Numeral can represent exactly any rational number.
14
+
15
+ Numerals can be exact or approximate. Exact numerals don't keep trailing zeros:
16
+ they don't specify a fixed precision. Repeating numerals are always exact.
17
+
18
+ Approximate numerals may have trailing zeros and have a determinate number
19
+ of significant digits. Approximate numerals cannot held repeating figures,
20
+ since they have limited precision.
21
+
22
+ The Conversions module provides conversions between Numerals and the
23
+ numeric types Integer, Rational, Float, Flt::Num and BigDecimal.
24
+
25
+ The Format class holds formatting settings. It can be constructed
26
+ with this bracket syntax:
27
+
28
+ format = Format[mode: :general, rounding: [precision: 10]]
29
+
30
+ Which is a shortcut for:
31
+
32
+ format = Format[mode: Format::Mode[:general], rounding: Rounding[precision: 10]]
33
+
34
+ And can also be expressed as:
35
+
36
+ format = Format[Format::Mode[:general], Rounding[precision: 10]]
37
+ puts format.rounding.precision # -> 10
38
+ puts format.rounding.mode # -> half_even
39
+
40
+ New formats can be derived from an existing one by overriding some of
41
+ its properties using the brackets operator on it:
42
+
43
+ format2 = format[rounding: :half_down]
44
+ puts format2.rounding.precision # -> 10
45
+ puts format2.rounding.mode # -> half_down
46
+
47
+ ## Output
48
+
49
+ Let's see how to use a Format to format a number into text form. By
50
+ default the shortest possible output (that preserves the value) is produced:
51
+
52
+ puts Format[].write(0.1) # -> 0.1
53
+
54
+ This is because the default Rounding property of Format is Rounding[:short]
55
+ (rounding to :short precision), so the above is equivalent to:
56
+
57
+ puts Format[:short].write(0.1) # -> 0.1
58
+ puts Format[rounding: :short].write(0.1) # -> 0.1
59
+
60
+ To produce a numeric representation that shows explicitly all the precision
61
+ of the number, the :free rounding precision can be used:
62
+
63
+ puts Format[:free].write(0.1) # -> 0.10000000000000001
64
+
65
+ Specific precision can be obtained like this:
66
+
67
+ puts Format[precision: 6].write(0.1) # -> 0.100000
68
+
69
+ But this won't show digits that are insignificant (when the input number
70
+ is regarded as an approximation):
71
+
72
+ puts Format[precision: 20].write(0.1) # -> 0.10000000000000001
73
+ puts Format[precision: 20].write(Rational(1,10))# -> 0.10000000000000000000
74
+
75
+ Although a Float is considered an approximation by default (since
76
+ it cannot represent arbitrary precision exactly), we can
77
+ reinterpret it as an exact quantity with the :exact_input Format option:
78
+
79
+ puts Format[:exact_input, precision: 20].write(0.1)# -> 0.10000000000000000555
80
+ puts Format[:exact_input].write(0.1) # -> 0.1000000000000000055511151231257827021181583404541015625
81
+
82
+ Rationals are always 'exact' quantities, and they may require infinite
83
+ digits to be represented exactly in some output bases. This is handled
84
+ by repeating numerals, which can be represented as text in two modes:
85
+
86
+ puts Format[].write(Rational(1,3)) # -> 0.333...
87
+ puts Format[symbols: [repeat_delimited: true]].write(Rational(1,3))# -> 0.<3>
88
+
89
+ ## Input
90
+
91
+ The same Format class can be used to read formatted text into numeric values
92
+ with the Format#read() method.
93
+
94
+ puts Format[].read('1.0', type: Float) # -> 1.0
95
+
96
+ For Flt types such as Flt::DecNum or Flt::BinNum there are a few options
97
+ to determine the result, since these types can hold arbitrary precision.
98
+
99
+ By default, these types are considered 'approximate'. Thus, the result
100
+ will be a variable-precision result based on the input. The default
101
+ Format, which has :short (simplifying) precision will produce a simple
102
+ result with as few significant digits as possible:
103
+
104
+ puts Format[:short].read('1.000', type: Flt::DecNum)# -> 1
105
+ puts Format[:short].read('0.100', type: Flt::BinNum)# -> 0.1
106
+
107
+ To retain the precision of the input text, the :free precision should be
108
+ used:
109
+
110
+ puts Format[:free].read('1.000', type: Flt::DecNum)# -> 1.000
111
+ puts Format[:free].read('0.100', type: Flt::BinNum)# -> 0.1
112
+
113
+ As an alternative, the precision implied by the text input can be ignored
114
+ and the result adjusted to the precision of the destination context. This
115
+ is done by regarding the input as 'exact'.
116
+
117
+ puts Format[:exact_input].read('1.000', type: Flt::DecNum)# -> 1.000000000000000000000000000
118
+ puts Format[:exact_input].read('0.100', type: Flt::BinNum)# -> 0.1
119
+ Flt::DecNum.context.precision = 8
120
+ puts Format[:exact_input].read('1.000', type: Flt::DecNum)# -> 1.0000000
121
+
122
+ If the input specifies repeating digits, then it is automatically regarded
123
+ exact and rounded according to the destination context:
124
+
125
+ puts Format[:exact_input].read('0.333...', type: Flt::DecNum)# -> 0.33333333
126
+
127
+ Note that the repeating digits have been automatically detected. This
128
+ happens because the repeating suffix '...' has ben found (it is defined
129
+ by the Format::Symbols property of Format). An alternative way of
130
+ specifying repeating digits is by the repeating delimiters specified
131
+ in Symbols, which are <> by default:
132
+
133
+ puts Format[:exact_input].read('0.<3>', type: Flt::DecNum)# -> 0.33333333
134
+
135
+
136
+ A Format can also be used to read a formatted number into a Numeral:
137
+
138
+ puts Format[].read('1.25', type: Numeral) # -> Numeral[1, 2, 5, :sign=>1, :point=>1, :normalize=>:approximate, :base=>10]
139
+ puts Format[].read('1.<3>', type: Numeral) # -> Numeral[1, 3, :sign=>1, :point=>1, :repeat=>1, :base=>10]
140
+
141
+ Other examples:
142
+
143
+ puts Format[:free, base: 2].read('0.1', type: Flt::DecNum)# -> 0.5
7
144
 
8
145
  Roadmap
9
146
  =======
@@ -17,8 +154,15 @@ Done:
17
154
 
18
155
  * Rounding can be applied to Numerals (with rounding options)
19
156
 
20
- Pending:
21
-
22
157
  * Numerals can be written into text form using Formatting options
23
158
 
24
159
  * Numerals con be read from text form using Formatting options
160
+
161
+ * Handling of 'unsignificant' digits: show them either as special
162
+ symbol, as zeros or omit them (comfigured in Symbols)
163
+
164
+ Pending:
165
+
166
+ * Padding aspect of formatting on output
167
+
168
+ * Show base indicators on output
@@ -1,30 +1,230 @@
1
1
  require 'numerals/conversions'
2
+ require 'bigdecimal'
3
+ require 'singleton'
2
4
 
3
- class Numerals::BigDecimalConversion
5
+ class Numerals::BigDecimalConversion < Numerals::ContextConversion
6
+
7
+ # Options:
8
+ #
9
+ # * :input_rounding (optional, a non-exact Rounding or rounding mode)
10
+ # which is used when input is approximate as the assumed rounding
11
+ # mode which would be used so that the result numeral rounds back
12
+ # to the input number
13
+ #
14
+ def initialize(options = {})
15
+ super BigDecimal, options
16
+ end
4
17
 
5
18
  def order_of_magnitude(value, options={})
6
19
  base = options[:base] || 10
7
20
  if base == 10
8
21
  value.exponent
9
22
  else
10
- Conversions.order_of_magnitude(Flt::DecNum(value.to_s), options)
23
+ (Math.log(value.abs)/Math.log(base)).floor + 1
11
24
  end
12
25
  end
13
26
 
14
- def number_to_numeral(number, options={})
15
- mode = options[:mode] || :fixed
27
+ def number_of_digits(value, options={})
16
28
  base = options[:base] || 10
17
- rounding = options[:rounding] || Rounding[:exact]
29
+ precision = x.precs.first
30
+ decimal_digits = x.split[1].size
31
+ n = decimal_digits # or use precision?
32
+ if base == 10
33
+ n
34
+ else
35
+ Flt::DecNum.context[precision: n].necessary_digits(base)
36
+ end
37
+ end
38
+
39
+ def exact?(value, options={})
40
+ options[:exact]
41
+ end
42
+
43
+ def number_to_numeral(number, mode, rounding)
44
+ if @context.special?(number)
45
+ special_num_to_numeral number
46
+ else
47
+ if mode == :exact
48
+ exact_num_to_numeral number, rounding
49
+ else # mode == :approximate
50
+ approximate_num_to_numeral(number, rounding)
51
+ end
52
+ end
53
+ end
54
+
55
+ def numeral_to_number(numeral, mode)
56
+ if numeral.special?
57
+ special_numeral_to_num numeral
58
+ elsif mode == :fixed
59
+ fixed_numeral_to_num numeral
60
+ else # mode == :free
61
+ free_numeral_to_num numeral
62
+ end
63
+ end
64
+
65
+ def write(number, exact_input, output_rounding)
66
+ output_base = output_rounding.base
67
+ input_base = @context.radix
68
+
69
+ if @context.special?(number)
70
+ special_num_to_numeral number
71
+ elsif exact_input
72
+ if output_base == input_base && output_rounding.free?
73
+ # akin to number.format(base: output_base, simplified: true)
74
+ if true
75
+ # ALT.1 just like approximate :short
76
+ general_num_to_numeral number, output_rounding, false
77
+ else
78
+ # ALT.2 just like different bases
79
+ exact_num_to_numeral number, output_rounding
80
+ end
81
+ else
82
+ # akin to number.format(base: output_base, exact: true)
83
+ exact_num_to_numeral number, output_rounding
84
+ end
85
+ else
86
+ if output_base == input_base && output_rounding.preserving?
87
+ # akin to number.format(base: output_base)
88
+ sign, coefficient, exponent = @context.split(number)
89
+ Numerals::Numeral.from_coefficient_scale sign*coefficient, exponent, approximate: true
90
+ elsif output_rounding.simplifying?
91
+ # akin to number.forma(base: output_base, simplify: true)
92
+ general_num_to_numeral number, output_rounding, false
93
+ else
94
+ # akin to number.forma(base: output_base, all_digits: true)
95
+ general_num_to_numeral number, output_rounding, true
96
+ end
97
+ end
98
+ end
99
+
100
+ def read(numeral, exact_input, approximate_simplified)
101
+ if numeral.special?
102
+ special_numeral_to_num numeral
103
+ elsif numeral.approximate? && !exact_input
104
+ if approximate_simplified
105
+ # akin to @context.Num(numeral_text, :short)
106
+ short_numeral_to_num numeral
107
+ else
108
+ # akin to @context.Num(numeral_text, :free)
109
+ free_numeral_to_num numeral
110
+ end
111
+ else
112
+ # akin to @context.Num(numeral_text, :fixed)
113
+ fixed_numeral_to_num numeral
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def special_num_to_numeral(x)
120
+ if x.nan?
121
+ Numerals::Numeral.nan
122
+ elsif x.infinite?
123
+ Numerals::Numeral.infinity @context.sign(x)
124
+ end
125
+ end
126
+
127
+ def exact_num_to_numeral(number, rounding)
128
+ quotient = number.to_r
129
+ numeral = Numerals::Numeral.from_quotient(quotient, base: rounding.base)
130
+ unless rounding.free?
131
+ numeral = rounding.round(numeral)
132
+ end
133
+ numeral
134
+ end
18
135
 
136
+ def approximate_num_to_numeral(number, rounding)
137
+ all_digits = !rounding.free?
138
+ general_num_to_numeral(number, rounding, all_digits)
19
139
  end
20
140
 
21
- def numeral_to_number(numeral, options={})
22
- mode = options[:mode] || :fixed
141
+ def general_num_to_numeral(x, rounding, all_digits)
142
+ sign, coefficient, exponent = @context.split(x)
143
+ # the actual number of digits is x.split[1].size
144
+ # but BigDecimal doesn't keep trailing zeros
145
+ # we'll use the internal precision which is an implementation detail
146
+ precision = x.precs.first
147
+ output_base = rounding.base
148
+
149
+ # here rounding_mode is not the output rounding mode, but the rounding mode used for input
150
+ rounding_mode = (@input_rounding || rounding).mode
151
+
152
+ # The minimum exponent of BigDecimal numbers is not well defined;
153
+ # depends of host architecture, version of BigDecimal, etc.
154
+ # We'll use an arbitrary conservative value.
155
+ min_exp = -100000000
156
+ formatter = Flt::Support::Formatter.new(
157
+ @context.radix, min_exp, output_base, raise_on_repeat: false
158
+ )
159
+ formatter.format(
160
+ x, coefficient, exponent, rounding_mode, precision, all_digits
161
+ )
162
+
163
+ dec_pos, digits = formatter.digits
23
164
 
165
+ normalization = :approximate
166
+
167
+ numeral = Numerals::Numeral[digits, sign: sign, point: dec_pos, rep_pos: formatter.repeat, base: output_base, normalize: normalization]
168
+
169
+ numeral = rounding.round(numeral, round_up: formatter.round_up)
170
+
171
+ numeral
172
+ end
173
+
174
+ def special_numeral_to_num(numeral)
175
+ case numeral.special
176
+ when :nan
177
+ @context.nan
178
+ when :inf
179
+ @context.infinity numeral.sign
180
+ end
181
+ end
182
+
183
+ def fixed_numeral_to_num(numeral)
184
+ # consider:
185
+ # return exact_numeral_to_num(numeral) if numeral.exact?
186
+ if numeral.base == 10
187
+ unless @context.exact?
188
+ rounding = Rounding[@context.rounding, precision: @context.precision, base: @context.radix]
189
+ numeral = rounding.round(numeral)
190
+ end
191
+ same_base_numeral_to_num numeral
192
+ else
193
+ general_numeral_to_num numeral, :fixed
194
+ end
195
+ end
196
+
197
+ def same_base_numeral_to_num(numeral)
198
+ sign, coefficient, scale = numeral.split
199
+ @context.Num sign, coefficient, scale
200
+ end
201
+
202
+ def exact_numeral_to_num(numeral)
203
+ @context.Num Rational(*numeral.to_quotient), :fixed
204
+ end
205
+
206
+ def free_numeral_to_num(numeral)
207
+ general_numeral_to_num numeral, :free
208
+ end
209
+
210
+ def general_numeral_to_num(numeral, mode)
211
+ sign, coefficient, scale = numeral.split
212
+ reader = Flt::Support::Reader.new(mode: mode)
213
+ if @input_rounding
214
+ rounding_mode = @input_rounding.mode
215
+ else
216
+ rounding_Mode = @context.rounding
217
+ end
218
+ dec_num_context = Flt::DecNum::Context(
219
+ precision: @context.precision,
220
+ rounding: @context.rounding
221
+ )
222
+ dec_num = reader.read(dec_num_context, rounding_mode, sign, coefficient, scale, numeral.base)
223
+ @context.Num dec_num
24
224
  end
25
225
 
26
226
  end
27
227
 
28
- def BigDecimal.numerals_conversion
29
- Numerals::BigDecimalConversion.new
228
+ def BigDecimal.numerals_conversion(options = {})
229
+ Numerals::BigDecimalConversion.new(options)
30
230
  end
@@ -0,0 +1,40 @@
1
+ require 'numerals/conversions'
2
+ require 'flt'
3
+
4
+ # Base class for Conversions of type with context
5
+ class Numerals::ContextConversion
6
+
7
+ def initialize(context_or_type, options={})
8
+ if Class === context_or_type && context_or_type.respond_to?(:context)
9
+ @type = context_or_type
10
+ @context = @type.context
11
+ elsif context_or_type.respond_to?(:num_class)
12
+ @context = context_or_type
13
+ @type = @context.num_class
14
+ else
15
+ raise "Invalid Conversion definition"
16
+ end
17
+ self.input_rounding = options[:input_rounding]
18
+ end
19
+
20
+ attr_reader :context, :type, :input_rounding
21
+
22
+ def input_rounding=(rounding)
23
+ if rounding
24
+ if rounding == :context
25
+ @input_rounding = Rounding[@context.rounding, precision: @context.precision, base: @context.radix]
26
+ else
27
+ rounding = Rounding[base: @context.radix].set!(rounding)
28
+ if rounding.base == @context.radix
29
+ @input_rounding = rounding
30
+ else
31
+ # The rounding precision is not meaningful for the destination type on input
32
+ @input_rounding = Rounding[rounding.mode, base: @context.radix]
33
+ end
34
+ end
35
+ else
36
+ @input_rounding = nil
37
+ end
38
+ end
39
+
40
+ end