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