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