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
@@ -0,0 +1,141 @@
1
+ module Numerals
2
+
3
+ class Format
4
+
5
+ class TextNotation < Notation
6
+
7
+ def assemble(output, text_parts)
8
+ if text_parts.special?
9
+ output << text_parts.special
10
+ else
11
+ output << text_parts.sign
12
+ output << text_parts.integer # or decide here if empty integer part is show as 0?
13
+ unless !text_parts.fractional? &&
14
+ !text_parts.repeat? &&
15
+ !format.symbols.show_point
16
+ output << format.symbols.point
17
+ end
18
+ output << text_parts.fractional
19
+ if text_parts.repeat?
20
+ if format.symbols.repeat_delimited
21
+ output << format.symbols.repeat_begin
22
+ output << text_parts.repeat
23
+ output << format.symbols.repeat_end
24
+ else
25
+ n = RepeatDetector.min_repeat_count(
26
+ text_parts.numeral.digits.digits_array,
27
+ text_parts.numeral.repeat,
28
+ format.symbols.repeat_count - 1
29
+ )
30
+ n.times do
31
+ output << text_parts.repeat
32
+ end
33
+ output << format.symbols.repeat_suffix
34
+ end
35
+ end
36
+ if text_parts.exponent_value != 0 || format.mode.mode == :scientific
37
+ output << format.symbols.exponent
38
+ output << text_parts.exponent
39
+ end
40
+ end
41
+ end
42
+
43
+ def disassemble(text)
44
+ text_parts = TextParts.new
45
+ s = format.symbols
46
+ special = /
47
+ \A
48
+ #{s.regexp(:plus, :minus, case_sensitivity: true)}?
49
+ \s*
50
+ #{s.regexp(:nan, :infinity, case_sensitivity: true)}
51
+ \Z
52
+ /x
53
+ if match = special.match(text)
54
+ valid = true
55
+ text_parts.special = "#{match[1]}#{match[2]}"
56
+ else
57
+ valid = true
58
+ base = format.significand_base
59
+ # TODO: replace numbered groups by named variables ?<var>
60
+ # TODO: ignore padding, admit base indicators
61
+ regular = /
62
+ \A
63
+ #{s.regexp(:plus, :minus)}?
64
+ \s*
65
+ (?:
66
+ (?:(#{s.regexp(:grouped_digits, base: base, no_capture: true)}+)#{s.regexp(:point)}?)
67
+ |
68
+ #{s.regexp(:point)} # admit empty integer part, but then a point is needed
69
+ )
70
+ (#{s.regexp(:digits, base: base, no_capture: true)}*)
71
+ (?:#{s.regexp(:repeat_begin)}(#{s.regexp(:digits, base: base, no_capture: true)}+)#{s.regexp(:repeat_end)})?
72
+ #{s.regexp(:repeat_suffix)}?
73
+ (?:#{s.regexp(:exponent)}#{s.regexp(:plus, :minus)}?(\d+))?
74
+ \Z
75
+ /x
76
+ unless s.case_sensitive?
77
+ regular = Regexp.new(regular.source, regular.options | Regexp::IGNORECASE)
78
+ end
79
+
80
+ match = regular.match(text)
81
+
82
+ if match.nil?
83
+ valid = false
84
+ else
85
+ # TODO: we could avoid capturing point, point_with_no_integer_part
86
+ sign = match[1]
87
+ integer_part = match[2]
88
+ point = match[3]
89
+ point_with_no_integer_part = match[4]
90
+ fractional_part = match[5]
91
+ repeat_begin = match[6]
92
+ repeat_part = match[7]
93
+ repeat_end = match[8]
94
+ repeat_suffix = match[9]
95
+ exponent = match[10]
96
+ exponent_sign = match[11]
97
+ exponent_value = match[12]
98
+
99
+ text_parts.sign = sign
100
+ text_parts.integer = integer_part
101
+ text_parts.fractional = fractional_part
102
+
103
+ if repeat_begin
104
+ if !repeat_part || !repeat_end || repeat_suffix
105
+ valid = false
106
+ end
107
+ text_parts.repeat = repeat_part
108
+ else
109
+ if repeat_part || repeat_end
110
+ valid = false
111
+ end
112
+ if repeat_suffix
113
+ text_parts.detect_repeat = true
114
+ end
115
+ end
116
+
117
+ text_parts.exponent_base = format.base
118
+ if exponent
119
+ if !exponent_value
120
+ valid = false
121
+ end
122
+ text_parts.exponent = "#{exponent_sign}#{exponent_value}"
123
+ text_parts.exponent_value = text_parts.exponent.to_i
124
+ else
125
+ if exponent_sign || exponent_value
126
+ valid = false
127
+ end
128
+ end
129
+ end
130
+ end
131
+ raise "Invalid text numeral" unless valid
132
+ text_parts
133
+ end
134
+
135
+ end
136
+
137
+ define_notation :text, TextNotation
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,167 @@
1
+ require 'stringio'
2
+
3
+ module Numerals
4
+
5
+ # Formatted output implementation
6
+ module Format::Output
7
+
8
+ def write(number, options={})
9
+ # 1. Convert number to numeral
10
+ numeral = conversion_out(number)
11
+ if numeral.approximate? && !@rounding.free?
12
+ insignificant_digits = @rounding.precision(numeral) - numeral.digits.size
13
+ if insignificant_digits > 0
14
+ numeral.expand! @rounding.precision(numeral)
15
+ end
16
+ end
17
+ return numeral if options[:output] == :numeral
18
+ # 2. Break numeral into parts (digits, etc.)
19
+ num_parts = partition_out(numeral, insignificant_digits: insignificant_digits)
20
+ if !@symbols.repeating && num_parts.repeating?
21
+ raise Format::InvalidRepeatingNumeral, "Invalid format: cannot represent exact value"
22
+ end
23
+ # 3. Represent parts as text
24
+ text_parts = symbolize_out(num_parts)
25
+ # 4. Assemble text parts into output notation
26
+ output = options[:output] || StringIO.new
27
+ assemble_out(output, text_parts)
28
+ options[:output] ? output : output.string
29
+ end
30
+
31
+ private
32
+
33
+ def digits_text(part, options = {})
34
+ @symbols.digits_text(part, options)
35
+ end
36
+
37
+ def grouped_digits_text(part, options = {})
38
+ @symbols.digits_text(part, options.merge(with_grouping: true))
39
+ end
40
+
41
+ def conversion_out(number) # => Numeral
42
+ return number if number.kind_of?(Numeral) # @exact_input is ignored
43
+ conversion_options = {
44
+ exact: exact_input,
45
+ rounding: rounding,
46
+ type_options: {
47
+ input_rounding: input_rounding_mode
48
+ }
49
+ }
50
+ Conversions.write(number, conversion_options)
51
+ end
52
+
53
+ def partition_out(numeral, options={}) # => num_parts (ExpSetting/BaseScaler)
54
+ num_parts = Format::ExpSetter[numeral, options]
55
+ return num_parts if numeral.special?
56
+ mode = @mode.mode
57
+ if mode == :general
58
+ mode = :fixed
59
+ if @mode.max_leading == :all
60
+ if numeral.repeating?
61
+ max_leading = 0
62
+ else
63
+ max_leading = @rounding.precision(numeral) - numeral.digits.size
64
+ end
65
+ else
66
+ max_leading = @mode.max_leading
67
+ end
68
+ check_trailing = !numeral.repeating?
69
+ if num_parts.leading_size > max_leading ||
70
+ check_trailing && (num_parts.trailing_size > @mode.max_trailing)
71
+ mode = :scientific
72
+ end
73
+ end
74
+
75
+ case mode
76
+ when :fixed
77
+ num_parts.exponent = 0
78
+ when :scientific
79
+ if @mode.sci_int_digits == :engineering
80
+ num_parts.integer_part_size = 1
81
+ num_parts.integer_part_size += 1 while (num_parts.exponent % 3) != 0
82
+ elsif @mode.sci_int_digits == :all
83
+ raise "Cannot represent number with integral significand" if numeral.repeating?
84
+ num_parts.integer_part_size = numeral.digits.size
85
+ else
86
+ num_parts.integer_part_size = @mode.sci_int_digits
87
+ end
88
+ end
89
+ if @mode.base_scale > 1 && !num_parts.special?
90
+ num_parts = Format::BaseScaler[num_parts, @mode.base_scale]
91
+ end
92
+
93
+ num_parts
94
+ end
95
+
96
+ def symbolize_out(num_parts) # => text_parts Hash
97
+ text_parts = TextParts.new(num_parts.numeral)
98
+ if num_parts.special?
99
+ case num_parts.special
100
+ when :nan
101
+ text_parts.special = @symbols.nan
102
+ when :inf
103
+ if num_parts.sign == -1
104
+ text_parts.special = @symbols.negative_infinity
105
+ else
106
+ text_parts.special = @symbols.positive_infinity
107
+ end
108
+ end
109
+ else
110
+ if num_parts.sign == -1
111
+ text_parts.sign = @symbols.minus
112
+ elsif @symbols.show_plus
113
+ text_parts.sign = @symbols.plus
114
+ end
115
+ if num_parts.integer_part.empty?
116
+ if @symbols.show_zero || (num_parts.fractional_part.empty? && !num_parts.repeat_part.empty?)
117
+ text_parts.integer = @symbols.zero
118
+ end
119
+ text_parts.integer_value = 0
120
+ else
121
+ if @symbols.insignificant_digit.nil?
122
+ # we can't just omit integer part symbols
123
+ integer_insignificant_digits = 0
124
+ else
125
+ integer_insignificant_digits = num_parts.integer_insignificant_size
126
+ end
127
+ text_parts.integer = grouped_digits_text(
128
+ num_parts.integer_part,
129
+ insignificant_digits: integer_insignificant_digits,
130
+ base: num_parts.base
131
+ )
132
+ text_parts.integer_value = Numerals::Digits[num_parts.integer_part, base: num_parts.base].value
133
+ end
134
+ text_parts.fractional = digits_text(
135
+ num_parts.fractional_part,
136
+ insignificant_digits: num_parts.fractional_insignificant_size,
137
+ baseL: num_parts.base
138
+ )
139
+ if num_parts.repeating?
140
+ text_parts.repeat = digits_text(
141
+ num_parts.repeat_part,
142
+ insignificant_digits: num_parts.repeat_insignificant_size,
143
+ base: num_parts.base
144
+ )
145
+ end
146
+ text_parts.exponent_value = num_parts.exponent
147
+ # if num_parts.exponent != 0 || @mode.mode == :scientific
148
+ text_parts.exponent = num_parts.exponent.to_s(10) # use digits_definition ?
149
+ if @symbols.show_exponent_plus && num_parts.exponent >= 0
150
+ text_parts.exponent = @symbols.plus + text_parts.exponent
151
+ end
152
+ # end
153
+ text_parts.exponent_base = num_parts.exponent_base.to_s(10) # use digits_definition ?
154
+ text_parts.exponent_base_value = num_parts.exponent_base
155
+ end
156
+ # TODO: justification
157
+ # TODO: base indicator for significand? significand_bas?
158
+ text_parts
159
+ end
160
+
161
+ def assemble_out(output, text_parts)
162
+ Format.assemble(@notation, output, self, text_parts)
163
+ end
164
+
165
+ end
166
+
167
+ end