Class: Numerals::Numeral

Inherits:
Object
  • Object
show all
Includes:
ModalSupport::BracketConstructor, ModalSupport::StateEquivalent
Defined in:
lib/numerals/numeral.rb

Overview

A Numeral represents a numeric value as a sequence of digits (possibly repeating) in some numeric base.

A numeral can have a special value (infinity or not-a-number).

A non-special numeral is defined by:

  • radix (the base)

  • digits (a Digits object)

  • sign (+1/-1)

  • point: the position of the fractional point; 0 would place it before the first digit, 1 before the second, etc.

  • repeat: the digits starting at this position repeat indefinitely

A Numeral is equivalent to a Rational number; a quotient of integers can be converted to a Numeral in any base and back to a quotient without altering its value (although the fraction might be simplified).

By default a Numeral represents an exact quantity (rational number). A numeral can also represent an approximate value with a specific precision: the number of significant digits (numeral.digits.size), which can include significant trailing zeros. Approximate numerals are never repeating.

Exact numerals are always repeating, but, when the repeating digits are just zeros, the repeating? method returns false.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Numeral

Special numerals may be constructed with the symbols :nan, :infinity, :negative_infinity, :positive_infinity (or with :infinity and the :sign option which should be either +1 or -1)

Examples:

Numeral[:nan]
Numeral[:infinity, sign: -1]

For non-special numerals, the first argument may be a Digits object or an Array of digits, and the remaining parameters (:base, :sign, :point and :repeat) are passed as options.

Examples:

Numeral[1,2,3, base: 10, point: 1] # 1.23
Numeral[1,2,3,4, point: 1, repeat: 2] # 1.234343434...

The :normalize option can be used to specify the kind of normalization to be applied to the numeral:

  • :exact, the default, produces a normalized :exact number where no trailing zeros are kept and there is always a repeat point (which may just repeat trailing zeros)

  • :approximate produces a non-repeating numeral with a fixed number of digits (where trailing zeros are significant)

  • false or nil will not normalize the result, mantaining the digits and repeat values passed.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/numerals/numeral.rb', line 83

def initialize(*args)
  if Hash === args.last
    options = args.pop
  else
    options = {}
  end
  options = { normalize: :exact }.merge(options)
  normalize = options.delete(:normalize)
  @point  = nil
  @repeat = nil
  @sign   = nil
  @radix  = options[:base] || options[:radix] || 10
  if args.size == 1 && Symbol === args.first
    @special = args.first
    case @special
    when :positive_infinity
      @special = :inf
      @sign = +1
    when :negative_infinity
      @special = :inf
      @sign = -1
    when :infinity
      @special = :inf
    end
  elsif args.size == 1 && Digits === args.first
    @digits = args.first
    @radix = @digits.radix || @radix
  elsif args.size == 1 && Array === args.first
    @digits = Digits[args.first, base: @radix]
  else
    if args.any? { |v| Symbol === v }
      @digits = Digits[base: @radix]
      args.each do |v|
        case v
        when :point
          @point = @digits.size
        when :repeat
          @repeat = @digits.size
        else # when Integer
          @digits.push v
        end
      end
    elsif args.size > 0
      @digits = Digits[args, base: @radix]
    end
  end
  if options[:value]
    @digits = Digits[value: options[:value], base: @radix]
  end
  @sign    ||= options[:sign] || +1
  @special ||= options[:special]
  unless @special
    @point   ||= options[:point]  || @digits.size
    @repeat  ||= options[:repeat] || @digits.size
  end
  case normalize
  when :exact
    normalize! Numeral.exact_normalization
  when :approximate
    normalize! Numeral.approximate_normalization
  when Hash
    normalize! normalize
  end
end

Instance Attribute Details

#digitsObject

Returns the value of attribute digits.



148
149
150
# File 'lib/numerals/numeral.rb', line 148

def digits
  @digits
end

#pointObject

Returns the value of attribute point.



148
149
150
# File 'lib/numerals/numeral.rb', line 148

def point
  @point
end

#radixObject

Returns the value of attribute radix.



148
149
150
# File 'lib/numerals/numeral.rb', line 148

def radix
  @radix
end

#repeatObject

Returns the value of attribute repeat.



148
149
150
# File 'lib/numerals/numeral.rb', line 148

def repeat
  @repeat
end

#signObject

Returns the value of attribute sign.



148
149
150
# File 'lib/numerals/numeral.rb', line 148

def sign
  @sign
end

#specialObject

Returns the value of attribute special.



148
149
150
# File 'lib/numerals/numeral.rb', line 148

def special
  @special
end

Class Method Details

.approximate_normalizationObject



222
223
224
# File 'lib/numerals/numeral.rb', line 222

def self.approximate_normalization
  { remove_extra_reps: false, remove_trailing_zeros: false, remove_leading_zeros: true, force_repeat: false }
end

.exact_normalizationObject



226
227
228
# File 'lib/numerals/numeral.rb', line 226

def self.exact_normalization
  { remove_extra_reps: true, remove_trailing_zeros: true, remove_leading_zeros: true, force_repeat: true }
end

.from_coefficient_scale(coefficient, scale, options = {}) ⇒ Object



510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/numerals/numeral.rb', line 510

def self.from_coefficient_scale(coefficient, scale, options={})
  radix = options[:base] || options[:radix] || 10
  if coefficient < 0
    sign = -1
    coefficient = -coefficient
  else
    sign = +1
  end
  digits = Digits[base: radix]
  digits.value = coefficient
  point = scale + digits.size
  normalization = options[:normalize] || :exact
  normalization = :approximate if options[:approximate]
  Numeral[digits, base: radix, point: point, sign: sign, normalize: normalization]
end

.from_quotient(*args) ⇒ Object

Create a Numeral from a quotient (Rational number). The quotient can be passed as an Array [numerator, denomnator]; to allow fractions with a zero denominator (representing indefinite or infinite numbers).



401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/numerals/numeral.rb', line 401

def self.from_quotient(*args)
  r = args.shift
  if Integer === args.first
    r = [r, args.shift]
  end
  options = args.shift || {}
  raise "Invalid number of arguments" unless args.empty?
  max_d = options.delete(:maximum_number_of_digits) || Numeral.maximum_number_of_digits
  if Rational === r
    x, y = r.numerator, r.denominator
  else
    x, y = r
  end
  return integer(x, options) if (x == 0 && y != 0) || y == 1

  radix = options[:base] || options[:radix] || 10

  xy_sign = x == 0 ? 0 : x < 0 ? -1 : +1
  xy_sign = -xy_sign if y < 0
  x = x.abs
  y = y.abs

  digits = Digits[base: radix]
  repeat = nil
  special = nil

  if y == 0
    if x == 0
      special = :nan
    else
      special = :inf
    end
  end

  return Numeral[special, sign: xy_sign] if special

  point = 1
  k = {}
  i = 0

  while (z = y*radix) < x
    y = z
    point += 1
  end

  while x > 0 && (max_d <= 0 || i < max_d)
    break if repeat = k[x]
    k[x] = i
    d, x = x.divmod(y)
    x *= radix
    digits.push d
    i += 1
  end

  while digits.size > 1 && digits.first == 0
    digits.shift
    repeat -= 1 if repeat
    point -= 1
  end

  Numeral[digits, sign: xy_sign, repeat: repeat, point: point]
end

.indeterminateObject



375
376
377
# File 'lib/numerals/numeral.rb', line 375

def self.indeterminate
  nan
end

.infinity(sign = +1) ⇒ Object



367
368
369
# File 'lib/numerals/numeral.rb', line 367

def self.infinity(sign=+1)
  Numeral[:inf, sign: sign]
end

.integer(x, options = {}) ⇒ Object



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/numerals/numeral.rb', line 379

def self.integer(x, options={})
  base = options[:base] || options[:radix] || 10
  if x == 0
    # we also could conventionally keep 0 either as Digits[[], ...]
    digits = Digits[0, base: base]
    sign = +1
  else
    if x < 0
      sign = -1
      x = -x
    else
      sign = +1
    end
    digits = Digits[value: x, base: base]
  end
  Numeral[digits, sign: sign]
end

.maximum_number_of_digitsObject

Return the maximum number of digits that Numeral objects can handle.



50
51
52
# File 'lib/numerals/numeral.rb', line 50

def self.maximum_number_of_digits
  @maximum_number_of_digits
end

.maximum_number_of_digits=(n) ⇒ Object

Change the maximum number of digits that Numeral objects can handle.



44
45
46
# File 'lib/numerals/numeral.rb', line 44

def self.maximum_number_of_digits=(n)
  @maximum_number_of_digits = [n, 2048].max
end

.nanObject



371
372
373
# File 'lib/numerals/numeral.rb', line 371

def self.nan
  Numeral[:nan]
end

.negative_infinityObject



363
364
365
# File 'lib/numerals/numeral.rb', line 363

def self.negative_infinity
  Numeral[:inf, sign: -1]
end

.positive_infinityObject



359
360
361
# File 'lib/numerals/numeral.rb', line 359

def self.positive_infinity
  Numeral[:inf, sign: +1]
end

.zero(options = {}) ⇒ Object



355
356
357
# File 'lib/numerals/numeral.rb', line 355

def self.zero(options={})
  integer 0, options
end

Instance Method Details

#-@Object



347
348
349
# File 'lib/numerals/numeral.rb', line 347

def -@
  negated
end

#approximate(number_of_digits) ⇒ Object



636
637
638
# File 'lib/numerals/numeral.rb', line 636

def approximate(number_of_digits)
  dup.approximate! number_of_digits
end

#approximate!(number_of_digits) ⇒ Object

Expand to the specified number of digits, then truncate and remove repetitions.



629
630
631
632
633
634
# File 'lib/numerals/numeral.rb', line 629

def approximate!(number_of_digits)
  expand! number_of_digits
  @digits.truncate! number_of_digits
  @repeat = nil
  self
end

#approximate?Boolean

An approximate Numeral has limited precision (number of significant digits). In an approximate Numeral, trailing zeros are significant.

Returns:

  • (Boolean)


605
606
607
# File 'lib/numerals/numeral.rb', line 605

def approximate?
  !exact?
end

#baseObject



150
151
152
# File 'lib/numerals/numeral.rb', line 150

def base
  @radix
end

#base=(b) ⇒ Object



154
155
156
# File 'lib/numerals/numeral.rb', line 154

def base=(b)
  @radix = b
end

#digit_value_at(i) ⇒ Object



207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/numerals/numeral.rb', line 207

def digit_value_at(i)
  if i < 0
    0
  elsif i < @digits.size
    @digits[i]
  elsif @repeat.nil? || @repeat >= @digits.size
    0
  else
    repeated_length = @digits.size - @repeat
    i = (i - @repeat) % repeated_length
    i += @repeat
    i < 0 ? 0 : @digits[i]
  end
end

#dupObject

Deep copy



332
333
334
335
336
# File 'lib/numerals/numeral.rb', line 332

def dup
  duped = super
  duped.digits = duped.digits.dup
  duped
end

#exactObject



644
645
646
# File 'lib/numerals/numeral.rb', line 644

def exact
  dup.exact!
end

#exact!Object



640
641
642
# File 'lib/numerals/numeral.rb', line 640

def exact!
  normalize! Numeral.exact_normalization
end

#exact?Boolean

An exact Numeral represents exactly a rational number. It always has a repeat position, although the repeated digits may all be zero.

Returns:

  • (Boolean)


599
600
601
# File 'lib/numerals/numeral.rb', line 599

def exact?
  !!@repeat
end

#expand(minimum_number_of_digits) ⇒ Object



623
624
625
# File 'lib/numerals/numeral.rb', line 623

def expand(minimum_number_of_digits)
  dup.expand! minimum_number_of_digits
end

#expand!(minimum_number_of_digits) ⇒ Object

Make sure the numeral has at least the given number of digits; This may denormalize the number.



611
612
613
614
615
616
617
618
619
620
621
# File 'lib/numerals/numeral.rb', line 611

def expand!(minimum_number_of_digits)
  if @repeat
    while @digits.size < minimum_number_of_digits
      @digits.push @digits[@repeat] || 0
      @repeat += 1
    end
  else
    @digits.push 0 while @digits.size < minimum_number_of_digits
  end
  self
end

#indeterminate?Boolean

Returns:

  • (Boolean)


170
171
172
# File 'lib/numerals/numeral.rb', line 170

def indeterminate?
  nan?
end

#infinite?Boolean

Returns:

  • (Boolean)


174
175
176
# File 'lib/numerals/numeral.rb', line 174

def infinite?
  @special == :inf
end

#inspectObject



592
593
594
# File 'lib/numerals/numeral.rb', line 592

def inspect
  to_s
end

#nan?Boolean

Returns:

  • (Boolean)


166
167
168
# File 'lib/numerals/numeral.rb', line 166

def nan?
  @special == :nan
end

#negate!Object



338
339
340
341
# File 'lib/numerals/numeral.rb', line 338

def negate!
  @sign = -@sign
  self
end

#negatedObject



343
344
345
# File 'lib/numerals/numeral.rb', line 343

def negated
  dup.negate!
end

#negative_infinite?Boolean

Returns:

  • (Boolean)


182
183
184
# File 'lib/numerals/numeral.rb', line 182

def negative_infinite?
  @special == :inf && @sign == -1
end

#nonrepeating?Boolean

Returns:

  • (Boolean)


199
200
201
# File 'lib/numerals/numeral.rb', line 199

def nonrepeating?
  !special && !repeating?
end

#normalize!(options = {}) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/numerals/numeral.rb', line 230

def normalize!(options = {})
  if @special
    if @special == :nan
      @sign = nil
    end
    @point = @repeat = nil
  else

    defaults = { remove_extra_reps: true, remove_trailing_zeros: true }
    options = defaults.merge(options)
    remove_trailing_zeros = options[:remove_trailing_zeros]
    remove_extra_reps = options[:remove_extra_reps]
    remove_leading_zeros = options[:remove_extra_reps]
    force_repeat = options[:force_repeat]

    # Remove unneeded repetitions
    if @repeat && remove_extra_reps
      rep_length = @digits.size - @repeat
      if rep_length > 0 && @digits.size >= 2*rep_length
        while @repeat > rep_length && @digits[@repeat, rep_length] == @digits[@repeat-rep_length, rep_length]
          @repeat -= rep_length
          @digits.replace @digits[0...-rep_length]
        end
      end
      # remove unneeded partial repetitions
      if rep_length > 0 && @digits.size > rep_length
        removed = 0
        while @repeat > 0 && @digits[@repeat-1] == @digits[@repeat-1+rep_length]
          @repeat -= 1
          removed += 1
        end
        @digits.replace @digits[0...-removed] if removed > 0
      end

    end

    # Replace 'nines' repetition 0.999... -> 1
    if @repeat && @repeat == @digits.size-1 && @digits[@repeat] == (@radix-1)
      @digits.pop
      @repeat = nil

      i = @digits.size - 1
      carry = 1
      while carry > 0 && i >= 0
        @digits[i] += carry
        carry = 0
        if @digits[i] > @radix
          carry = 1
          @digits[i] = 0
          @digits.pop if i == @digits.size
        end
        i -= 1
      end
      if carry > 0
        digits.unshift carry
        @point += 1
      end
    end

    # Remove zeros repetitions
    if remove_trailing_zeros
      if @repeat && @repeat >= @digits.size
        @repeat = @digits.size
      end
      if @repeat && @repeat >= 0
        unless @digits[@repeat..-1].any? { |x| x != 0 }
          @digits.replace @digits[0...@repeat]
          @repeat = nil
        end
      end
    end

    if force_repeat
      @repeat ||= @digits.size
    else
      @repeat = nil if @repeat && @repeat >= @digits.size
    end

    # Remove leading zeros
    if remove_leading_zeros
      # if all digits are zero, we consider all to be trailing zeros
      unless !remove_trailing_zeros && @digits.zero?
        while @digits.first == 0
          @digits.shift
          @repeat -= 1 if @repeat
          @point -= 1
        end
      end
    end

    # Remove trailing zeros
    if remove_trailing_zeros && !repeating?
      while @digits.last == 0
        @digits.pop
        @repeat -= 1 if @repeat
      end
    end
  end
  self
end

#normalized(options = {}) ⇒ Object



351
352
353
# File 'lib/numerals/numeral.rb', line 351

def normalized(options={})
  dup.normalize! options
end

#parametersObject



550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'lib/numerals/numeral.rb', line 550

def parameters
  if special?
    params = { special: @special }
    params.merge! sign: @sign if @special == :inf
  else
    params = {
      digits: @digits,
      sign:   @sign,
      point:  @point
    }
    params.merge! repeat: @repeat if @repeat
    if approximate?
      params.merge! normalize: :approximate
    end
  end
  params
end

#positive_infinite?Boolean

Returns:

  • (Boolean)


178
179
180
# File 'lib/numerals/numeral.rb', line 178

def positive_infinite?
  @special == :inf && @sign == +1
end

#repeating?Boolean

Returns:

  • (Boolean)


195
196
197
# File 'lib/numerals/numeral.rb', line 195

def repeating?
  !special? && @repeat && @repeat < @digits.size
end

#repeating_positionObject

unlike the repeat attribute, this is nevel nil



191
192
193
# File 'lib/numerals/numeral.rb', line 191

def repeating_position
  @repeat || @digits.size
end

#scaleObject



158
159
160
# File 'lib/numerals/numeral.rb', line 158

def scale
  @point - @digits.size
end

#scale=(s) ⇒ Object



203
204
205
# File 'lib/numerals/numeral.rb', line 203

def scale=(s)
  @point = s + @digits.size
end

#special?Boolean

Returns:

  • (Boolean)


162
163
164
# File 'lib/numerals/numeral.rb', line 162

def special?
  !!@special
end

#splitObject



526
527
528
529
530
531
# File 'lib/numerals/numeral.rb', line 526

def split
  if @special || (@repeat && @repeat < @digits.size)
    raise NumeralError, "Numeral cannot be represented as sign, coefficient, scale"
  end
  [@sign, @digits.value, scale]
end

#to_base(other_base) ⇒ Object

Convert a Numeral to a different base



541
542
543
544
545
546
547
548
# File 'lib/numerals/numeral.rb', line 541

def to_base(other_base)
  if other_base == @radix
    dup
  else
    normalization = exact? ? :exact : :approximate
    Numeral.from_quotient to_quotient, base: other_base, normalize: normalization
  end
end

#to_quotientObject

Return a quotient (Rational) that represents the exact value of the numeral. The quotient is returned as an Array, so that fractions with a zero denominator can be handled (representing indefinite or infinite numbers).



467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/numerals/numeral.rb', line 467

def to_quotient
  if @special
    y = 0
    case @special
    when :nan
      x = 0
    when :inf
      x = @sign
    end
    return [x, y]
  end

  n = @digits.size
  a = 0
  b = a

  repeat = @repeat
  repeat = nil if repeat && repeat >= n

  for i in 0...n
    a *= @radix
    a += @digits[i]
    if repeat && i < repeat
      b *= @radix
      b += @digits[i]
    end
  end

  x = a
  x -= b if repeat

  y = @radix**(n - @point)
  y -= @radix**(repeat - @point) if repeat

  d = Numerals.gcd(x, y)
  x /= d
  y /= d

  x = -x if @sign < 0

  [x.to_i, y.to_i]
end

#to_sObject



568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/numerals/numeral.rb', line 568

def to_s
  case @special
  when :nan
    'Numeral[:nan]'
  when :inf
    if @sign < 0
      'Numeral[:inf, sign: -1]'
    else
      'Numeral[:inf]'
    end
  else
    args = ''
    if @digits.size > 0
      args = @digits.digits_array.to_s.unwrap('[]')
      args << ', '
    end
    params = parameters
    params.delete :digits
    params.merge! base: @radix
    args << params.to_s.unwrap('{}')
    "Numeral[#{args}]"
  end
end

#to_value_scaleObject



533
534
535
536
537
538
# File 'lib/numerals/numeral.rb', line 533

def to_value_scale
  if @special || (@repeat && @repeat < @digits.size)
    raise NumeralError, "Numeral cannot be represented as value, scale"
  end
  [@digits.value*@sign, scale]
end

#zero?Boolean

Returns:

  • (Boolean)


186
187
188
# File 'lib/numerals/numeral.rb', line 186

def zero?
  !special? && @digits.zero?
end