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.
- checksums.yaml +4 -4
- data/README.md +149 -5
- data/lib/numerals/conversions/bigdecimal.rb +209 -9
- data/lib/numerals/conversions/context_conversion.rb +40 -0
- data/lib/numerals/conversions/float.rb +106 -71
- data/lib/numerals/conversions/flt.rb +115 -44
- data/lib/numerals/conversions/integer.rb +32 -3
- data/lib/numerals/conversions/rational.rb +27 -3
- data/lib/numerals/conversions.rb +74 -33
- data/lib/numerals/digits.rb +8 -5
- data/lib/numerals/format/base_scaler.rb +160 -0
- data/lib/numerals/format/exp_setter.rb +218 -0
- data/lib/numerals/format/format.rb +257 -0
- data/lib/numerals/format/input.rb +140 -0
- data/lib/numerals/format/mode.rb +157 -0
- data/lib/numerals/format/notation.rb +51 -0
- data/lib/numerals/format/notations/html.rb +53 -0
- data/lib/numerals/format/notations/latex.rb +48 -0
- data/lib/numerals/format/notations/text.rb +141 -0
- data/lib/numerals/format/output.rb +167 -0
- data/lib/numerals/format/symbols.rb +565 -0
- data/lib/numerals/format/text_parts.rb +35 -0
- data/lib/numerals/format.rb +25 -0
- data/lib/numerals/formatting_aspect.rb +36 -0
- data/lib/numerals/numeral.rb +34 -21
- data/lib/numerals/repeat_detector.rb +99 -0
- data/lib/numerals/rounding.rb +340 -181
- data/lib/numerals/version.rb +1 -1
- data/lib/numerals.rb +4 -2
- data/numerals.gemspec +1 -1
- data/test/test_base_scaler.rb +189 -0
- data/test/test_big_conversions.rb +105 -0
- data/test/test_digits_definition.rb +23 -28
- data/test/test_exp_setter.rb +732 -0
- data/test/test_float_conversions.rb +48 -30
- data/test/test_flt_conversions.rb +476 -80
- data/test/test_format.rb +124 -0
- data/test/test_format_input.rb +226 -0
- data/test/test_format_mode.rb +124 -0
- data/test/test_format_output.rb +789 -0
- data/test/test_integer_conversions.rb +22 -22
- data/test/test_numeral.rb +35 -0
- data/test/test_rational_conversions.rb +28 -28
- data/test/test_repeat_detector.rb +72 -0
- data/test/test_rounding.rb +158 -0
- data/test/test_symbols.rb +32 -0
- metadata +38 -5
- data/lib/numerals/formatting/digits_definition.rb +0 -75
@@ -0,0 +1,257 @@
|
|
1
|
+
module Numerals
|
2
|
+
|
3
|
+
# A Format object holds formatting options and performs
|
4
|
+
# formatted input/output operations on numbers.
|
5
|
+
#
|
6
|
+
# Formatting options are grouped into aspects:
|
7
|
+
#
|
8
|
+
# * Exact input
|
9
|
+
# * Rounding
|
10
|
+
# * Mode
|
11
|
+
# * Symbols
|
12
|
+
# * Input rounding
|
13
|
+
#
|
14
|
+
# Some aspects (Rounding, Mode & Symbols) are handled with aspect-definining
|
15
|
+
# classes Rounding, Format::Mode and Format::Symbols.
|
16
|
+
#
|
17
|
+
# Exact input applies only to numeric types that can hold limited
|
18
|
+
# precision values such as Float, Flt::Num or BigDecimal. It specifies
|
19
|
+
# that the numeric value is to be taken as an exact quantity. Otherwise,
|
20
|
+
# the numeric value is interpreted as a rounded approximation of some
|
21
|
+
# original exact value (so it represents a range of exact number which
|
22
|
+
# would all be rounded to the same approximation). Rational and Integer
|
23
|
+
# types are always exact and not affected by this option.
|
24
|
+
#
|
25
|
+
# Rounding defines how numbers are rounded into text form or how
|
26
|
+
# values represented in text round to numeric values. So it specifies
|
27
|
+
# the precision of the result and whether the result is an approximation
|
28
|
+
# or an exact quantity.
|
29
|
+
#
|
30
|
+
# Mode defines de formatting style.
|
31
|
+
#
|
32
|
+
# Symbols contains the details of how digits and other symbols are
|
33
|
+
# represented in text form and the final text notation used.
|
34
|
+
#
|
35
|
+
# The input-rounding property can set to either a Rounding object
|
36
|
+
# or just a rounding mode symbol (:half_even, etc.).
|
37
|
+
# It is used to define which rounding mode is implied when reading textual
|
38
|
+
# numeric expressions into approximate numeric values. It affects how
|
39
|
+
# approximate numbers are written to text because the text representation
|
40
|
+
# of approximate values should be read back into the original value.
|
41
|
+
# If a Rounding object is assigned only the mode is used, and it is ignored
|
42
|
+
# if the rounding is exact. #
|
43
|
+
#
|
44
|
+
class Format < FormattingAspect
|
45
|
+
|
46
|
+
def initialize(*args)
|
47
|
+
@exact_input = false
|
48
|
+
@rounding = Rounding[:short]
|
49
|
+
@mode = Mode[]
|
50
|
+
@symbols = Symbols[]
|
51
|
+
@notation = :text
|
52
|
+
@input_rounding = nil
|
53
|
+
set! *args
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :rounding, :exact_input, :mode, :symbols, :notation,
|
57
|
+
:input_rounding
|
58
|
+
|
59
|
+
def base
|
60
|
+
@rounding.base
|
61
|
+
end
|
62
|
+
|
63
|
+
# Presentation base for the significand
|
64
|
+
def significand_base
|
65
|
+
base**@mode.base_scale
|
66
|
+
end
|
67
|
+
|
68
|
+
include ModalSupport::StateEquivalent
|
69
|
+
|
70
|
+
def input_rounding?
|
71
|
+
!@input_rounding.nil?
|
72
|
+
end
|
73
|
+
|
74
|
+
def input_rounding_mode
|
75
|
+
input_rounding? ? @input_rounding.mode : nil
|
76
|
+
end
|
77
|
+
|
78
|
+
set do |*args|
|
79
|
+
options = extract_options(*args)
|
80
|
+
@exact_input = options[:exact_input] if options.has_key?(:exact_input)
|
81
|
+
@rounding.set! options[:rounding] if options[:rounding]
|
82
|
+
@mode.set! options[:mode] if options[:mode] # :format ?
|
83
|
+
@symbols.set! options[:symbols] if options[:symbols]
|
84
|
+
@notation = options[:notation] if options[:notation]
|
85
|
+
if options.has_key?(:input_rounding)
|
86
|
+
set_input_rounding! options[:input_rounding]
|
87
|
+
end
|
88
|
+
|
89
|
+
# shortcuts
|
90
|
+
@rounding.set! base: options[:base] if options[:base]
|
91
|
+
@symbols.set! digits: options[:digits] if options[:digits]
|
92
|
+
@rounding.set! mode: options[:rounding_mode] if options[:rounding_mode]
|
93
|
+
@rounding.set! precision: options[:precision] if options[:precision]
|
94
|
+
@rounding.set! places: options[:places] if options[:places]
|
95
|
+
@symbols.set! repeating: options[:repeating] if options.has_key?(:repeating)
|
96
|
+
@symbols.set! case_sensitive: options[:case_sensitive] if options.has_key?(:case_sensitive)
|
97
|
+
end
|
98
|
+
|
99
|
+
def parameters
|
100
|
+
{
|
101
|
+
rounding: @rounding,
|
102
|
+
exact_input: @exact_input,
|
103
|
+
mode: @mode,
|
104
|
+
symbols: @symbols,
|
105
|
+
notation: @notation,
|
106
|
+
input_rounding: input_rounding? ? @input_rounding : nil
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_s
|
111
|
+
args = []
|
112
|
+
args << "exact_input: true" if @exact_input
|
113
|
+
args << "rounding: #{@rounding}"
|
114
|
+
args << "mode: #{@mode}"
|
115
|
+
args << "symbols: #{@symbols}"
|
116
|
+
args << "notation: #{@notation.inspect}" if @notation != :text
|
117
|
+
args << "input_rounding: #{input_rounding_mode.inspect}" if input_rounding?
|
118
|
+
"Format[#{args.join(', ')}]"
|
119
|
+
end
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
to_s
|
123
|
+
end
|
124
|
+
|
125
|
+
def case_sensitive?
|
126
|
+
@symbols.case_sensitive?
|
127
|
+
end
|
128
|
+
|
129
|
+
aspect :rounding do |*args|
|
130
|
+
set! rounding: args
|
131
|
+
end
|
132
|
+
|
133
|
+
aspect :base do |base|
|
134
|
+
set! base: base
|
135
|
+
end
|
136
|
+
|
137
|
+
aspect :exact_input do |value|
|
138
|
+
@exact_input = value
|
139
|
+
end
|
140
|
+
|
141
|
+
aspect :mode do |*args|
|
142
|
+
set! mode: args
|
143
|
+
end
|
144
|
+
|
145
|
+
aspect :symbols do |*args|
|
146
|
+
set symbols: args
|
147
|
+
end
|
148
|
+
|
149
|
+
aspect :notation do |notation|
|
150
|
+
set! notation: notation
|
151
|
+
end
|
152
|
+
|
153
|
+
aspect :digits do |digits|
|
154
|
+
set! digits: digits
|
155
|
+
end
|
156
|
+
|
157
|
+
aspect :input_rounding do |input_roundig|
|
158
|
+
if input_roundig.nil?
|
159
|
+
@input_rounding = nil
|
160
|
+
else
|
161
|
+
if @input_rounding.nil?
|
162
|
+
@input_rounding = Rounding[input_roundig]
|
163
|
+
else
|
164
|
+
@input_rounding.set! input_roundig
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def dup
|
170
|
+
# we need deep copy
|
171
|
+
Format[parameters]
|
172
|
+
end
|
173
|
+
|
174
|
+
include Output
|
175
|
+
include Input
|
176
|
+
|
177
|
+
# Shortcuts to Symbols sub-aspects
|
178
|
+
|
179
|
+
aspect :repeat do |*args|
|
180
|
+
@symbols.set_repeat!(*args)
|
181
|
+
end
|
182
|
+
|
183
|
+
aspect :grouping do |*args|
|
184
|
+
@symbols.set_grouping!(*args)
|
185
|
+
end
|
186
|
+
|
187
|
+
aspect :group_thousands do |sep = nil|
|
188
|
+
@symbols.set_grouping!(sep)
|
189
|
+
end
|
190
|
+
|
191
|
+
aspect :signs do |plus, minus|
|
192
|
+
@symbols.set_signs!(plus, minus)
|
193
|
+
end
|
194
|
+
|
195
|
+
aspect :plus do |plus, which = nil|
|
196
|
+
@symbols.set_plus!(plus, which)
|
197
|
+
end
|
198
|
+
|
199
|
+
aspect :minus do |minus|
|
200
|
+
@symbols.set_minus!(minus)
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def extract_options(*args)
|
206
|
+
options = {}
|
207
|
+
args = args.first if args.size == 1 && args.first.kind_of?(Array)
|
208
|
+
args.each do |arg|
|
209
|
+
case arg
|
210
|
+
when Hash
|
211
|
+
options.merge! arg
|
212
|
+
when Rounding
|
213
|
+
options[:rounding] = arg
|
214
|
+
when Mode
|
215
|
+
options[:mode] = arg
|
216
|
+
when Symbols
|
217
|
+
options[:symbols] = arg
|
218
|
+
when Symbols::Digits
|
219
|
+
options[:digits] = arg
|
220
|
+
when Format
|
221
|
+
options.merge! arg.parameters
|
222
|
+
when :exact_input
|
223
|
+
options[:exact_input] = true
|
224
|
+
when :hexbin
|
225
|
+
options.merge!(
|
226
|
+
base: 2,
|
227
|
+
mode: {
|
228
|
+
base_scale: 4,
|
229
|
+
mode: :scientific,
|
230
|
+
sci_int_digits: 1
|
231
|
+
},
|
232
|
+
symbols: {
|
233
|
+
exponent: 'p'
|
234
|
+
}
|
235
|
+
)
|
236
|
+
when :gen, :general, :sci, :scientific, :fix; :fixed
|
237
|
+
options[:mode] = Mode[arg]
|
238
|
+
when :short, :free, :simplify, :preserve
|
239
|
+
options[:precision] = arg
|
240
|
+
when :half_even, :half_down, :half_up, :down, :up, :ceiling, :floor, :up05
|
241
|
+
options[:rounding_mode] = arg
|
242
|
+
when :case_sensitive
|
243
|
+
options[:case_sensitive] = true
|
244
|
+
when Symbol
|
245
|
+
options[:notation] = arg
|
246
|
+
when Integer
|
247
|
+
options[:base] = arg
|
248
|
+
else
|
249
|
+
raise "Invalid Format definition"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
options
|
253
|
+
end
|
254
|
+
|
255
|
+
end
|
256
|
+
|
257
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Numerals
|
2
|
+
|
3
|
+
# Formatted input implementation
|
4
|
+
module Format::Input
|
5
|
+
|
6
|
+
def read(text, options={})
|
7
|
+
# 1. Obtain destination type
|
8
|
+
selector = options[:context] || options[:type]
|
9
|
+
conversion = Conversions[selector, options[:type_options]]
|
10
|
+
if conversion
|
11
|
+
type = conversion.type
|
12
|
+
else
|
13
|
+
type = options[:type]
|
14
|
+
if type != Numeral
|
15
|
+
raise Format::InvalidNumericType, "Invalid type #{selector.inspect}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# if conversion.is_a?(ContextConversion)
|
20
|
+
# context = conversion.context
|
21
|
+
# end
|
22
|
+
|
23
|
+
# 2. dissassemble (parse notation): text notation => text parts
|
24
|
+
text_parts = Format.disassemble(@notation, self, text)
|
25
|
+
|
26
|
+
# 3. Convert text parts to values and generate a Numeral
|
27
|
+
numeral = partition_in(text_parts)
|
28
|
+
|
29
|
+
# 4. Convert to requested type:
|
30
|
+
if type == Numeral
|
31
|
+
return numeral
|
32
|
+
else
|
33
|
+
conversion_in(numeral, options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def partition_in(text_parts)
|
38
|
+
if text_parts.special?
|
39
|
+
# Read special number
|
40
|
+
nan = /#{@symbols.regexp(:nan, case_sensitivity: true)}/
|
41
|
+
inf = /
|
42
|
+
#{@symbols.regexp(:plus, :minus, case_sensitivity: true)}?
|
43
|
+
\s*
|
44
|
+
#{@symbols.regexp(:infinity, case_sensitivity: true)}
|
45
|
+
/x
|
46
|
+
minus = /#{@symbols.regexp(:minus, case_sensitivity: true)}/
|
47
|
+
if nan.match(text_parts.special)
|
48
|
+
numeral = Numeral[:nan]
|
49
|
+
elsif match = inf.match(text_parts.special)
|
50
|
+
if match[1] && match[1] =~ minus
|
51
|
+
sign = -1
|
52
|
+
else
|
53
|
+
sign = +1
|
54
|
+
end
|
55
|
+
numeral = Numeral[:infinity, sign: sign]
|
56
|
+
else
|
57
|
+
raise Format::InvaludNumberFormat, "Invalid number"
|
58
|
+
end
|
59
|
+
else
|
60
|
+
# Parse and convert text parts to values
|
61
|
+
input_rounding = @input_rounding || @rounding
|
62
|
+
input_base = significand_base
|
63
|
+
|
64
|
+
if !@symbols.repeating && (text_parts.repeat || text_parts.detect_repeat)
|
65
|
+
raise Format::InvalidRepeatingNumeral, "Invalid format: unexpected repeating numeral"
|
66
|
+
end
|
67
|
+
|
68
|
+
minus = /#{@symbols.regexp(:minus, case_sensitivity: true)}/
|
69
|
+
if text_parts.sign? && text_parts.sign =~ minus
|
70
|
+
sign = -1
|
71
|
+
else
|
72
|
+
sign = +1
|
73
|
+
end
|
74
|
+
|
75
|
+
integer_digits = []
|
76
|
+
if text_parts.integer?
|
77
|
+
integer_digits = @symbols.digits_values(text_parts.integer, base: input_base)
|
78
|
+
end
|
79
|
+
|
80
|
+
fractional_digits = []
|
81
|
+
if text_parts.fractional?
|
82
|
+
fractional_digits = @symbols.digits_values(text_parts.fractional, base: input_base)
|
83
|
+
end
|
84
|
+
|
85
|
+
exponent_value = 0
|
86
|
+
if text_parts.exponent?
|
87
|
+
exponent_value = text_parts.exponent.to_i
|
88
|
+
end
|
89
|
+
|
90
|
+
point = integer_digits.size
|
91
|
+
|
92
|
+
if text_parts.detect_repeat? # repeat_suffix found
|
93
|
+
digits = integer_digits + fractional_digits
|
94
|
+
digits, repeat = RepeatDetector.detect(digits, @symbols.repeat_count - 1)
|
95
|
+
elsif text_parts.repeat?
|
96
|
+
repeat_digits = @symbols.digits_values(text_parts.repeat, base: input_base)
|
97
|
+
digits = integer_digits + fractional_digits
|
98
|
+
repeat = digits.size
|
99
|
+
digits += repeat_digits
|
100
|
+
else
|
101
|
+
digits = integer_digits + fractional_digits
|
102
|
+
repeat = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
if @mode.base_scale > 1
|
106
|
+
# De-scale the significand base
|
107
|
+
digits = Format::BaseScaler.ugrouped_digits(digits, base, @mode.base_scale)
|
108
|
+
point *= @mode.base_scale
|
109
|
+
repeat *= @mode.base_scale if repeat
|
110
|
+
end
|
111
|
+
|
112
|
+
point += exponent_value
|
113
|
+
|
114
|
+
# Generate Numeral
|
115
|
+
if repeat || @exact_input
|
116
|
+
normalization = :exact
|
117
|
+
else
|
118
|
+
normalization = :approximate
|
119
|
+
end
|
120
|
+
numeral = Numeral[digits, sign: sign, point: point, repeat: repeat, base: base, normalize: normalization]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def conversion_in(numeral, options)
|
125
|
+
options = options.merge(
|
126
|
+
exact: numeral.exact?, # @exact_input, Also: if we want to force :fixed format
|
127
|
+
simplify: @rounding.simplifying?, # applies to approx input only => :short
|
128
|
+
)
|
129
|
+
type_options = { input_rounding: @input_rounding || @rounding }
|
130
|
+
if options[:type_options]
|
131
|
+
options[:type_options] = type_options.merge(options[:type_options])
|
132
|
+
else
|
133
|
+
options[:type_options] = type_options
|
134
|
+
end
|
135
|
+
Conversions.read(numeral, options)
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Numerals
|
2
|
+
|
3
|
+
# Formatting mode
|
4
|
+
#
|
5
|
+
# * :scientific use scientific notation.
|
6
|
+
# * :fixed used fixed notation.
|
7
|
+
# * :general (the default) uses :fixed notation unless
|
8
|
+
# it would produce trailing zeros in the integer part
|
9
|
+
# or too many leading zeros in the fractional part.
|
10
|
+
# The intent is to produced the simplest or more natural
|
11
|
+
# output and it's regulated by the :max_leading and
|
12
|
+
# :max_trailing parameters.
|
13
|
+
#
|
14
|
+
# The special value :engineering can be used as a shortcut
|
15
|
+
# for :scientific mode with :engineering :sci_int_digits.
|
16
|
+
#
|
17
|
+
# The modes can be abbreviated as :sci, :fix, :gen and :end.
|
18
|
+
#
|
19
|
+
# * :sci_int_digits numbe of digits to the left of the point
|
20
|
+
# in scientific notation. By default 1. The special value
|
21
|
+
# :engineering can be used for engineering notation, i.e.
|
22
|
+
# the number of digits will be between one and 3 so that the
|
23
|
+
# exponent is a multiple of 3.
|
24
|
+
# The special value :all is used to make the significand an
|
25
|
+
# integer value.
|
26
|
+
#
|
27
|
+
# * :max_leading (maximum number of leading zeros) determines
|
28
|
+
# when :scientific is chosen instead of :fixed when the mode is
|
29
|
+
# :general.
|
30
|
+
# The default value, 5 is that of the General Decimal Arithmetic
|
31
|
+
# 'to-scientific-string' number to text conversion, and also used
|
32
|
+
# by the .NET General ("G") format specifier. To reproduce the
|
33
|
+
# behaviour of the C %g format specifier the value should be 3.
|
34
|
+
# The special value :all can be used to reproduce the behaviour
|
35
|
+
# of some calculators, where scientific notation is used when
|
36
|
+
# more digits than the specified precision would be needed.
|
37
|
+
#
|
38
|
+
class Format::Mode < FormattingAspect
|
39
|
+
|
40
|
+
DEFAULTS = {
|
41
|
+
mode: :general,
|
42
|
+
sci_int_digits: 1,
|
43
|
+
max_leading: 5,
|
44
|
+
max_trailing: 0,
|
45
|
+
base_scale: 1
|
46
|
+
}
|
47
|
+
|
48
|
+
def initialize(*args)
|
49
|
+
DEFAULTS.each do |param, value|
|
50
|
+
instance_variable_set "@#{param}", value
|
51
|
+
end
|
52
|
+
set! *args
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :mode, :sci_int_digits, :max_leading, :max_trailing, :base_scale
|
56
|
+
|
57
|
+
include ModalSupport::StateEquivalent
|
58
|
+
|
59
|
+
set do |*args|
|
60
|
+
options = extract_options(*args)
|
61
|
+
options.each do |option, value|
|
62
|
+
send :"#{option}=", value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
MODE_SHORTCUTS = {
|
67
|
+
gen: :general,
|
68
|
+
sci: :scientific,
|
69
|
+
fix: :fixed,
|
70
|
+
eng: :engineering
|
71
|
+
}
|
72
|
+
|
73
|
+
def mode=(mode)
|
74
|
+
@mode = MODE_SHORTCUTS[mode] || mode
|
75
|
+
if @mode == :engineering
|
76
|
+
@mode = :scientific
|
77
|
+
@sci_int_digits = :engineering
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def sci_int_digits=(v)
|
82
|
+
@sci_int_digits = v
|
83
|
+
if @sci_int_digits == :eng
|
84
|
+
@sci_int_digits = :engineering
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
attr_writer :max_leading, :max_trailing, :base_scale
|
89
|
+
|
90
|
+
def engineering?
|
91
|
+
@mode == :scientific && @sci_int_digits == :engineering
|
92
|
+
end
|
93
|
+
|
94
|
+
def scientific?
|
95
|
+
@mode == :scientific
|
96
|
+
end
|
97
|
+
|
98
|
+
def fixed?
|
99
|
+
@mode == :fixed
|
100
|
+
end
|
101
|
+
|
102
|
+
def general?
|
103
|
+
@mode == :general
|
104
|
+
end
|
105
|
+
|
106
|
+
def parameters(abbreviated=false)
|
107
|
+
params = {}
|
108
|
+
DEFAULTS.each do |param, default|
|
109
|
+
value = instance_variable_get("@#{param}")
|
110
|
+
if !abbreviated || value != default
|
111
|
+
params[param] = value
|
112
|
+
end
|
113
|
+
end
|
114
|
+
if abbreviated && engineering?
|
115
|
+
params[:mode] = :engineering
|
116
|
+
params.delete :sci_int_digits
|
117
|
+
end
|
118
|
+
params
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_s
|
122
|
+
"Mode[#{parameters(true).inspect.unwrap('{}')}]"
|
123
|
+
end
|
124
|
+
|
125
|
+
def inspect
|
126
|
+
"Format::#{self}"
|
127
|
+
end
|
128
|
+
|
129
|
+
# Note: since Mode has no mutable attributes, default dup is OK
|
130
|
+
# otherwise we'd need to redefine it:
|
131
|
+
# def dup
|
132
|
+
# Mode[parameters]
|
133
|
+
# end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def extract_options(*args)
|
138
|
+
options = {}
|
139
|
+
args = args.first if args.size == 1 && args.first.kind_of?(Array)
|
140
|
+
args.each do |arg|
|
141
|
+
case arg
|
142
|
+
when Hash
|
143
|
+
options.merge! arg
|
144
|
+
when Symbol
|
145
|
+
options[:mode] = arg
|
146
|
+
when Format::Mode
|
147
|
+
options.merge! arg.parameters
|
148
|
+
else
|
149
|
+
raise "Invalid Mode definition"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
options
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Numerals
|
2
|
+
|
3
|
+
class Format
|
4
|
+
|
5
|
+
class Notation
|
6
|
+
|
7
|
+
def initialize(format)
|
8
|
+
@format = format
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :format
|
12
|
+
|
13
|
+
def assemble(output, text_parts)
|
14
|
+
raise "assemble must be implemented in Notation derived class #{self.class}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def disassemble(text)
|
18
|
+
raise "disassemble must be implemented in Notation derived class #{self.class}"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
@notations = {}
|
24
|
+
|
25
|
+
def self.define_notation(id, notation_class)
|
26
|
+
unless notation_class.class == Class && notation_class.superclass == Notation
|
27
|
+
raise "Notation class must be derived from Format::Notation"
|
28
|
+
end
|
29
|
+
@notations[id] = notation_class
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.notation(id, format)
|
33
|
+
@notations[id].new(format) || raise("Unknown notation #{id.inspect}")
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def self.assemble(id, output, format, text_parts)
|
38
|
+
notation(id, format).assemble(output, text_parts)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.disassemble(id, format, text)
|
42
|
+
notation(id, format).disassemble(text)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
require 'numerals/format/notations/text'
|
50
|
+
require 'numerals/format/notations/latex'
|
51
|
+
require 'numerals/format/notations/html'
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Numerals
|
4
|
+
|
5
|
+
class Format
|
6
|
+
|
7
|
+
class HtmlNotation < Notation
|
8
|
+
|
9
|
+
def assemble(output, text_parts)
|
10
|
+
# 1.23<span style="text-decoration: overline">456</span> ×10<sup>9</sup>
|
11
|
+
# Or alternative: use classes
|
12
|
+
# <span class=”numerals-num”>1.23<span class="numerals-rep">456</span> ×10<span class="numerals-sup">9</span></span>
|
13
|
+
# .numerals-rep { text-decoration: overline; }
|
14
|
+
# .numerals-sup { vertical-align: super; }
|
15
|
+
if text_parts.special?
|
16
|
+
output << escape(text_parts.special)
|
17
|
+
else
|
18
|
+
output << escape(text_parts.sign)
|
19
|
+
output << escape(text_parts.integer) # or decide here if empty integer part is show as 0?
|
20
|
+
unless !text_parts.fractional? &&
|
21
|
+
!text_parts.repeat? &&
|
22
|
+
!format.symbols.show_point
|
23
|
+
output << escape(format.symbols.point)
|
24
|
+
end
|
25
|
+
output << escape(text_parts.fractional)
|
26
|
+
if text_parts.repeat
|
27
|
+
output << %(<span style="text-decoration: overline">#{escape(text_parts.repeat)}</span>)
|
28
|
+
end
|
29
|
+
if text_parts.exponent_value != 0 || format.mode.mode == :scientific
|
30
|
+
output << "×"
|
31
|
+
output << escape(text_parts.exponent_base)
|
32
|
+
output << "<sup>#{escape(text_parts.exponent)}</sup>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def escape(text)
|
40
|
+
CGI.escapeHTML(text)
|
41
|
+
end
|
42
|
+
|
43
|
+
def unescape(text)
|
44
|
+
CGI.unescapeHTML(text)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
define_notation :html, HtmlNotation
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Numerals
|
2
|
+
|
3
|
+
class Format
|
4
|
+
|
5
|
+
class LatexNotation < Notation
|
6
|
+
|
7
|
+
def assemble(output, text_parts)
|
8
|
+
# 1.23\overline{456}\times10^{9}
|
9
|
+
if text_parts.special?
|
10
|
+
output << text_parts.special
|
11
|
+
else
|
12
|
+
output << text_parts.sign
|
13
|
+
output << text_parts.integer # or decide here if empty integer part is shown as 0?
|
14
|
+
unless !text_parts.fractional? &&
|
15
|
+
!text_parts.repeat? &&
|
16
|
+
!format.symbols.show_point
|
17
|
+
output << format.symbols.point
|
18
|
+
end
|
19
|
+
output << text_parts.fractional
|
20
|
+
if text_parts.repeat?
|
21
|
+
output << "\\overline{#{text_parts.repeat}}"
|
22
|
+
end
|
23
|
+
if text_parts.exponent_value != 0 || format.mode.mode == :scientific
|
24
|
+
output << "\\times"
|
25
|
+
output << text_parts.exponent_base
|
26
|
+
output << "^"
|
27
|
+
output << "{#{text_parts.exponent}}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def escape(text)
|
35
|
+
text.gsub('\\', '\\\\')
|
36
|
+
end
|
37
|
+
|
38
|
+
def unescape(text)
|
39
|
+
text.gsub('\\\\', '\\')
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
define_notation :latex, LatexNotation
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|