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
@@ -1,229 +1,388 @@
1
- # Rounding of Numerals
2
- class Rounding
3
-
4
- # Rounding is defined by the rounding mode and the precision,
5
- # and is used to stablish the desired accuracy of a Numeral result.
6
- #
7
- # The rounding mode may be any of the valid Flt rounding modes
8
- # (:half_even, :half_down, :half_up, :floor, :ceiling, :down, :up or :up05)
9
- # or :exact for no rounding at all (to represent the situation where
10
- # we want to get an exact numeral result.
11
- #
12
- # The precision may be defined either as a relative :precision value
13
- # (number of significant digits of the result numeral) or as an :absolute
14
- # :places that indicate the number of digits to the right of the fractional
15
- # point. Negative values of :places will round to digits in integral positions.
16
- #
17
- # The base of the numerals to be rounded must also be defined (10 by default)
18
- #
19
- def initialize(*args)
20
- if Hash === args.last
21
- options = args.pop.dup
22
- else
23
- options = {}
24
- end
25
- args.each do |arg|
26
- if Symbol === arg
27
- options[:mode] = arg
28
- elsif Integer === arg
29
- options[:precision] = arg
30
- else
31
- raise "Invalid Rounding definition"
1
+ module Numerals
2
+
3
+ # Rounding of Numerals
4
+ class Rounding < FormattingAspect
5
+
6
+ # Rounding defines a rounding mode and a precision,
7
+ # and is used to establish the desired accuracy of a Numeral result.
8
+ #
9
+ # Rounding also defines the base of the numerals to be rounded,
10
+ # which is 10 by default.
11
+ #
12
+ # The rounding mode is the rule used to limit
13
+ # the precision of a numeral; the rounding modes available are those
14
+ # of Flt::Num, namely:
15
+ #
16
+ # * :half_even
17
+ # * :half_up
18
+ # * :half_down
19
+ # * :ceiling
20
+ # * :floor
21
+ # * :up
22
+ # * :down
23
+ # * :up05
24
+ #
25
+ # Regarding the rounding precision there are two types of Roundings:
26
+ #
27
+ # * Fixed (limited) precision: the precision of the rounded result is either
28
+ # defined as relative (number of significant digits defined by the
29
+ # precision property) or absolute (number of fractional places
30
+ # --decimals for base 10-- defined by the places property)
31
+ # * Free (unlimited) precision, which preserves the value of
32
+ # the input numeral. As much precision as needed is used to keep
33
+ # unambiguously the original value. When applied to exact input,
34
+ # this kind of rounding doesn't perform any rounding.
35
+ # For approximate input there are two variants:
36
+ # - Preserving the original value precision, which produces and
37
+ # approximate output. (All original digits are preserved;
38
+ # full precision mode). This is the default free precision mode,
39
+ # established by using the :free symbol for the precision
40
+ # (or its synonym :preserve).
41
+ # - Simplifiying or reducing the result to produce an exact output
42
+ # without unneeded digits to restore the original value within its
43
+ # original precision (e.g. traling zeros are not keep).
44
+ # This case can be defined with the :short symbol for the precision
45
+ # (or its synonum :simplify).
46
+ #
47
+ def initialize(*args)
48
+ DEFAULTS.each do |param, value|
49
+ instance_variable_set "@#{param}", value
32
50
  end
51
+ set! *args
33
52
  end
34
- assign! options
35
- end
36
53
 
37
- attr_reader :mode, :base
54
+ DEFAULTS = {
55
+ mode: :half_even,
56
+ precision: :short,
57
+ places: nil,
58
+ base: 10
59
+ }
38
60
 
39
- include ModalSupport::BracketConstructor
40
- include ModalSupport::StateEquivalent
61
+ attr_reader :mode, :base
41
62
 
42
- def [](options={})
43
- assign! options
44
- end
63
+ include ModalSupport::StateEquivalent
45
64
 
46
- def assign!(options)
47
- @mode = options[:mode] || @mode
48
- @precision = options[:precision] || @precision
49
- @places = options[:places] || @places
50
- @base = options[:base] || @base
65
+ set do |*args|
66
+ options = extract_options(*args)
67
+ options.each do |option, value|
68
+ send :"#{option}=", value
69
+ end
70
+ end
51
71
 
52
- if (@precision == 0 || @precision.nil?) && @places.nil?
53
- @precision = 0
54
- @mode = :exact
55
- else
56
- @mode ||= :half_even
72
+ def base=(v)
73
+ @base = v
57
74
  end
58
- if @mode == :exact
59
- @precision = 0
60
- @places = nil
75
+
76
+ def mode=(mode)
77
+ @mode = mode
61
78
  end
62
- @base ||= 10
63
- self
64
- end
65
79
 
66
- def parameters
67
- if exact?
68
- { mode: :exact }
69
- elsif relative?
70
- { mode: @mode, precision: @precision }
71
- elsif absolute?
72
- { mode: @mode, places: @places }
80
+ def precision=(v)
81
+ @precision = v
82
+ @precision = :simplify if v == 0
83
+ @places = nil if @precision
73
84
  end
74
- end
75
85
 
76
- def to_s
77
- if exact?
78
- "Rounding[:exact]"
79
- elsif relative?
80
- "Rounding[#{@mode.inspect}, precision: #{@precision}]"
81
- elsif absolute?
82
- "Rounding[#{@mode.inspect}, places: #{@places}]"
86
+ def places=(v)
87
+ @places = v
88
+ @precision = nil if @places
83
89
  end
84
- end
85
90
 
86
- def inspect
87
- to_s
88
- end
91
+ def parameters
92
+ if @precision
93
+ { mode: @mode, precision: @precision, base: @base }
94
+ else
95
+ { mode: @mode, places: @places, base: @base }
96
+ end
97
+ end
89
98
 
90
- def exact?
91
- @mode == :exact
92
- end
99
+ def to_s
100
+ params = parameters
101
+ DEFAULTS.each do |param, default|
102
+ params.delete param if params[param] == default
103
+ end
104
+ "Rounding[#{params.inspect.unwrap('{}')}]"
105
+ end
93
106
 
94
- def absolute?
95
- !exact? && !@precision
96
- end
107
+ def inspect
108
+ to_s
109
+ end
97
110
 
98
- def relative?
99
- !exact? && !absolute?
100
- end
111
+ # Returns true if the Rounding is of free (unlimited) precision,
112
+ # which can be either :free (preserving) or :short (simplifying)
113
+ # regarding approximate input.
114
+ def free? # unlimited? exact? all? nonrounding? free?
115
+ [:free, :short].include?(@precision)
116
+ end
101
117
 
102
- # Number of significant digits for a given numerical/numeral value
103
- def precision(value)
104
- if relative? || exact?
105
- @precision
106
- else
107
- @places + num_integral_digits(value)
118
+ # Returns true if the Rounding is of fixed (limited) precision.
119
+ def fixed? # limited? approximate? rounding? fixed?
120
+ !free?
108
121
  end
109
- end
110
122
 
111
- # Number of fractional placesfor a given numerical/numeral value
112
- def places(value)
113
- if absolute? || exact?
114
- @places
115
- else
116
- @precision - num_integral_digits(value)
123
+ # Returns true if the Rounding is of fixed precision defined
124
+ # as a number of fractional places, i.e. independently of the
125
+ # number to be rounded's magnitude.
126
+ def absolute?
127
+ @precision.nil? # fixed? && @precision # !@places.nil?
117
128
  end
118
- end
119
129
 
120
- # Round a numeral. If the numeral has been truncated
121
- # the :round_up option must be used to pass the information
122
- # about the discarded digits:
123
- # * nil if all discarded digits where 0 (the truncated value is exact)
124
- # * :lo if there where non-zero discarded digits, but the first discarded digit
125
- # is below half the base.
126
- # * :tie if the first discarded was half the base and there where no more nonzero digits,
127
- # i.e. the original value was a 'tie', exactly halfway between the truncated value
128
- # and the next value with the same number of digits.
129
- # * :hi if the original value was above the tie value.
130
- def round(numeral, options={})
131
- round_up = options[:round_up]
132
- numeral, round_up = truncate(numeral, round_up)
133
- adjust(numeral, round_up)
134
- end
130
+ # Returns true if the Rounding is of fixed precision defined
131
+ # as a number of significant digits (precision attribute),
132
+ # i.e. in relation to the number to be rounded's magnitude.
133
+ def relative?
134
+ fixed? && !absolute?
135
+ end
135
136
 
136
- private
137
+ # Returns true if the Rounding is of free precision and
138
+ # the behaviour for approximate numbers is producing a
139
+ # simplified (short) result with only the needed digits
140
+ # to restore the original value within its precision.
141
+ def simplifying?
142
+ @precision == :short
143
+ end
137
144
 
138
- def check_base(numeral)
139
- if numeral.base != @base
140
- raise "Invalid Numeral (base #{numeral.base}) for a base #{@base} Rounding"
145
+ def short?
146
+ simplifying?
147
+ end
148
+
149
+ # Returns true if the Rounding is of free precision and
150
+ # the behaviour for approximate numbers is to keep its
151
+ # original precision (so it may include trailing zeros)
152
+ # and the result of rounding will be an approximate numeral.
153
+ def preserving?
154
+ @precision == :free
155
+ end
156
+
157
+ def full?
158
+ preserving?
141
159
  end
142
- end
143
160
 
144
- # Truncate a numeral and return also a round_up value with information about
145
- # the digits beyond the truncation point that can be used to round the truncated
146
- # numeral. If the numeral has already been truncated, the round_up result of
147
- # that truncation should be passed as the second argument.
148
- def truncate(numeral, round_up=nil)
149
- check_base numeral
150
- if exact?
151
- round_up = nil
152
- else
153
- n = precision(numeral)
154
- unless n==numeral.digits.size && numeral.approximate?
155
- if n < numeral.digits.size - 1
156
- rest_digits = numeral.digits[n+1..-1]
161
+ # Number of significant digits for a given numerical/numeral value.
162
+ # If no value is passed, the :precision property is returned.
163
+ def precision(value = nil, options = {})
164
+ if value.nil?
165
+ @precision
166
+ elsif free?
167
+ if is_exact?(value, options)
168
+ 0
157
169
  else
158
- rest_digits = []
170
+ num_digits(value, options)
159
171
  end
160
- if numeral.repeating? && numeral.repeat < numeral.digits.size && n >= numeral.repeat
161
- rest_digits += numeral.digits[numeral.repeat..-1]
172
+ else # fixed?
173
+ if absolute?
174
+ @places + num_integral_digits(value)
175
+ else # relative?
176
+ @precision
162
177
  end
163
- digits = numeral.digits[0, n]
164
- if digits.size < n
165
- digits += (digits.size...n).map{|i| numeral.digit_value_at(i)}
178
+ end
179
+ end
180
+
181
+ # Number of fractional places for a given numerical/numeral value
182
+ # If no value is passed, the :places property is returned.
183
+ def places(value = nil, options = {})
184
+ if value.nil?
185
+ @places
186
+ elsif is_exact?(value, options)
187
+ @places || 0
188
+ elsif free?
189
+ num_digits(value, options) - num_integral_digits(value)
190
+ else # fixed?
191
+ if absolute?
192
+ @places
193
+ else # relative?
194
+ @precision - num_integral_digits(value)
166
195
  end
167
- if numeral.base % 2 == 0
168
- tie_digit = numeral.base / 2
169
- max_lo = tie_digit - 1
170
- else
171
- max_lo = numeral.base / 2
196
+ end
197
+ end
198
+
199
+ # Round a numeral. If the numeral has been truncated
200
+ # the :round_up option must be used to pass the information
201
+ # about the discarded digits:
202
+ # * nil if all discarded digits where 0 (the truncated value is exact)
203
+ # * :lo if there where non-zero discarded digits, but the first discarded digit
204
+ # is below half the base.
205
+ # * :tie if the first discarded was half the base and there where no more nonzero digits,
206
+ # i.e. the original value was a 'tie', exactly halfway between the truncated value
207
+ # and the next value with the same number of digits.
208
+ # * :hi if the original value was above the tie value.
209
+ def round(numeral, options={})
210
+ round_up = options[:round_up]
211
+ numeral, round_up = truncate(numeral, round_up)
212
+ if numeral.exact?
213
+ numeral
214
+ else
215
+ adjust(numeral, round_up)
216
+ end
217
+ end
218
+
219
+ # Note: since Rounding has no mutable attributes, default dup is OK
220
+ # otherwise we'd need to redefine it:
221
+ # def dup
222
+ # Rounding[parameters]
223
+ # end
224
+
225
+ private
226
+
227
+ def check_base(numeral)
228
+ if numeral.base != @base
229
+ raise "Invalid Numeral (base #{numeral.base}) for a base #{@base} Rounding"
230
+ end
231
+ end
232
+
233
+ # Truncate a numeral and return also a round_up value with information about
234
+ # the digits beyond the truncation point that can be used to round the truncated
235
+ # numeral. If the numeral has already been truncated, the round_up result of
236
+ # that prior truncation should be passed as the second argument.
237
+
238
+
239
+ def truncate(numeral, round_up=nil)
240
+ check_base numeral
241
+ unless simplifying? # TODO: could simplify this just skiping on free?
242
+ n = precision(numeral)
243
+ if n == 0
244
+ return numeral if numeral.repeating? # or rails inexact...
245
+ n = numeral.digits.size
172
246
  end
173
- next_digit = numeral.digit_value_at(n)
174
- if next_digit == 0
175
- unless round_up.nil? && rest_digits.all?{|d| d == 0}
176
- round_up = :lo
247
+ unless n >= numeral.digits.size && numeral.approximate?
248
+ if n < numeral.digits.size - 1
249
+ rest_digits = numeral.digits[n+1..-1]
250
+ else
251
+ rest_digits = []
177
252
  end
178
- elsif next_digit <= max_lo # next_digit < tie_digit
179
- round_up = :lo
180
- elsif next_digit == tie_digit
181
- if round_up || rest_digits.any?{|d| d != 0}
182
- round_up = :hi
253
+ if numeral.repeating? && numeral.repeat < numeral.digits.size && n >= numeral.repeat
254
+ rest_digits += numeral.digits[numeral.repeat..-1]
255
+ end
256
+ digits = numeral.digits[0, n]
257
+ if digits.size < n
258
+ digits += (digits.size...n).map{|i| numeral.digit_value_at(i)}
259
+ end
260
+ if numeral.base % 2 == 0
261
+ tie_digit = numeral.base / 2
262
+ max_lo = tie_digit - 1
183
263
  else
184
- round_up = :tie
264
+ max_lo = numeral.base / 2
265
+ end
266
+ next_digit = numeral.digit_value_at(n)
267
+ if next_digit == 0
268
+ unless round_up.nil? && rest_digits.all?{|d| d == 0}
269
+ round_up = :lo
270
+ end
271
+ elsif next_digit <= max_lo # next_digit < tie_digit
272
+ round_up = :lo
273
+ elsif next_digit == tie_digit
274
+ if round_up || rest_digits.any?{|d| d != 0}
275
+ round_up = :hi
276
+ else
277
+ round_up = :tie
278
+ end
279
+ else # next_digit > tie_digit
280
+ round_up = :hi
185
281
  end
186
- else # next_digit > tie_digit
187
- round_up = :hi
282
+ numeral = Numeral[
283
+ digits, point: numeral.point, sign: numeral.sign,
284
+ base: numeral.base,
285
+ normalize: :approximate
286
+ ]
188
287
  end
189
- numeral = Numeral[digits, point: numeral.point, sign: numeral.sign, normalize: :approximate]
190
288
  end
289
+ [numeral, round_up]
191
290
  end
192
- [numeral, round_up]
193
- end
194
291
 
195
- # Adjust a truncated numeral using the round-up information
196
- def adjust(numeral, round_up)
197
- check_base numeral
198
- point, digits = Flt::Support.adjust_digits(
199
- numeral.point, numeral.digits.digits_array,
200
- round_mode: @mode,
201
- negative: numeral.sign == -1,
202
- round_up: round_up,
203
- base: numeral.base
204
- )
205
- Numeral[digits, point: point, base: numeral.base, sign: numeral.sign, normalize: :approximate]
206
- end
292
+ # Adjust a truncated numeral using the round-up information
293
+ def adjust(numeral, round_up)
294
+ check_base numeral
295
+ point, digits = Flt::Support.adjust_digits(
296
+ numeral.point, numeral.digits.digits_array,
297
+ round_mode: @mode,
298
+ negative: numeral.sign == -1,
299
+ round_up: round_up,
300
+ base: numeral.base
301
+ )
302
+ if numeral.zero? && simplifying?
303
+ digits = []
304
+ point = 0
305
+ end
306
+ normalization = simplifying? ? :exact : :approximate
307
+ Numeral[digits, point: point, base: numeral.base, sign: numeral.sign, normalize: normalization]
308
+ end
207
309
 
208
- ZERO_DIGITS = 0 # 1?
310
+ ZERO_DIGITS = 0 # 1?
209
311
 
210
- # Number of digits in the integer part of the value (excluding leading zeros).
211
- def num_integral_digits(value)
212
- case value
213
- when 0
214
- ZERO_DIGITS
215
- when Numeral
216
- if value.zero?
312
+ # Number of digits in the integer part of the value (excluding leading zeros).
313
+ def num_integral_digits(value)
314
+ case value
315
+ when 0
217
316
  ZERO_DIGITS
317
+ when Numeral
318
+ if value.zero?
319
+ ZERO_DIGITS
320
+ else
321
+ if @base != value.base
322
+ value = value.to_base(@base)
323
+ end
324
+ value.normalized(remove_trailing_zeros: true).point
325
+ end
218
326
  else
219
- if @base != value.base
220
- value = value.to_base(@base)
327
+ Conversions.order_of_magnitude(value, base: @base)
328
+ end
329
+ end
330
+
331
+ def num_digits(value, options)
332
+ case value
333
+ when 0
334
+ ZERO_DIGITS
335
+ when Numeral
336
+ if value.zero?
337
+ ZERO_DIGITS
338
+ else
339
+ if @base != value.base
340
+ value = value.to_base(@base)
341
+ end
342
+ if value.repeating?
343
+ 0
344
+ else
345
+ value.digits.size
346
+ end
221
347
  end
222
- value.normalized(remove_trailing_zeros: true).point
348
+ else
349
+ Conversions.number_of_digits(value, options.merge(base: @base))
223
350
  end
224
- else
225
- Conversions.order_of_magnitude(value, base: @base)
226
351
  end
352
+
353
+ def is_exact?(value, options={})
354
+ case value
355
+ when Numeral
356
+ value.exact?
357
+ else
358
+ Conversions.exact?(value, options)
359
+ end
360
+ end
361
+
362
+ def extract_options(*args)
363
+ options = {}
364
+ args = args.first if args.size == 1 && args.first.kind_of?(Array)
365
+ args.each do |arg|
366
+ case arg
367
+ when Hash
368
+ options.merge! arg
369
+ when :short, :simplify
370
+ options.merge! precision: :short
371
+ when :free, :preserve
372
+ options.merge! precision: :free
373
+ when Symbol
374
+ options[:mode] = arg
375
+ when Integer
376
+ options[:precision] = arg
377
+ when Rounding
378
+ options.merge! arg.parameters
379
+ else
380
+ raise "Invalid Rounding definition"
381
+ end
382
+ end
383
+ options
384
+ end
385
+
227
386
  end
228
387
 
229
388
  end
@@ -1,3 +1,3 @@
1
1
  module Numerals
2
- VERSION = "0.0.0"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/numerals.rb CHANGED
@@ -2,11 +2,13 @@ require 'modalsupport'
2
2
 
3
3
  require "numerals/version"
4
4
  require 'numerals/numeral'
5
+ require 'numerals/formatting_aspect'
5
6
  require 'numerals/rounding'
7
+ require 'numerals/repeat_detector'
6
8
  require 'numerals/conversions/float'
7
9
  require 'numerals/conversions/integer'
8
10
  require 'numerals/conversions/rational'
9
11
  require 'numerals/conversions/bigdecimal'
10
12
  require 'numerals/conversions/flt'
11
- require 'numerals/formatting/digits_definition'
12
- require 'numerals/formatting/options'
13
+ require 'numerals/format'
14
+ # require 'numerals/formatting/options'
data/numerals.gemspec CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency 'flt', "~> 1.3.4"
21
+ spec.add_dependency 'flt', "~> 1.4.6"
22
22
  spec.add_dependency 'modalsupport', "~> 0.9.2"
23
23
 
24
24
  spec.add_development_dependency "bundler", "~> 1.6"