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
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