plurimath 0.11.0 → 0.11.1
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/.rubocop_todo.yml +354 -34
- data/Gemfile +2 -2
- data/README.adoc +49 -9
- data/lib/plurimath/deprecation.rb +2 -1
- data/lib/plurimath/errors/configuration_error.rb +5 -1
- data/lib/plurimath/errors/deprecation_error.rb +2 -1
- data/lib/plurimath/errors/invalid_number.rb +17 -0
- data/lib/plurimath/errors/unsupported_base.rb +18 -0
- data/lib/plurimath/errors/{formatter/unsupported_locale.rb → unsupported_locale.rb} +1 -1
- data/lib/plurimath/errors.rb +8 -0
- data/lib/plurimath/formatter/numbers/base_notation.rb +7 -2
- data/lib/plurimath/formatter/numbers/format_options.rb +123 -17
- data/lib/plurimath/formatter/numbers/fraction.rb +7 -16
- data/lib/plurimath/formatter/numbers/integer.rb +3 -0
- data/lib/plurimath/formatter/numbers/notation_renderer.rb +35 -6
- data/lib/plurimath/formatter/numbers/number_renderer.rb +16 -4
- data/lib/plurimath/formatter/numbers/precision_resolver.rb +16 -3
- data/lib/plurimath/formatter/numbers/sign_renderer.rb +2 -1
- data/lib/plurimath/formatter/numbers/significant.rb +4 -2
- data/lib/plurimath/formatter/numbers/source.rb +18 -3
- data/lib/plurimath/formatter/standard.rb +6 -0
- data/lib/plurimath/formatter/supported_locales.rb +1 -1
- data/lib/plurimath/formatter.rb +0 -2
- data/lib/plurimath/html/transform.rb +24 -12
- data/lib/plurimath/math/core.rb +17 -14
- data/lib/plurimath/math/function/overleftrightarrow.rb +2 -2
- data/lib/plurimath/math/number.rb +11 -1
- data/lib/plurimath/math.rb +4 -1
- data/lib/plurimath/number_formatter.rb +45 -4
- data/lib/plurimath/omml/formula_transformation.rb +2 -1
- data/lib/plurimath/omml/translator.rb +4 -1
- data/lib/plurimath/unicode_math/parse.rb +93 -53
- data/lib/plurimath/unicode_math/parser.rb +7 -4
- data/lib/plurimath/unicode_math/parsing_rules/absence_rules.rb +33 -21
- data/lib/plurimath/unicode_math/parsing_rules/common_rules.rb +28 -16
- data/lib/plurimath/unicode_math/parsing_rules/constants_rules.rb +18 -7
- data/lib/plurimath/unicode_math/parsing_rules/masked.rb +35 -15
- data/lib/plurimath/unicode_math/parsing_rules/sub_sup.rb +98 -72
- data/lib/plurimath/unicode_math/transform.rb +755 -468
- data/lib/plurimath/version.rb +1 -1
- data/lib/plurimath.rb +1 -0
- metadata +5 -4
- data/lib/plurimath/errors/formatter/unsupported_base.rb +0 -21
data/README.adoc
CHANGED
|
@@ -513,7 +513,7 @@ formatter = Plurimath::NumberFormatter.new(
|
|
|
513
513
|
<1> Locale to be used for number formatting.
|
|
514
514
|
<2> String pattern to define the number formatting.
|
|
515
515
|
<3> Hash containing relevant options for number formatting.
|
|
516
|
-
<4> Number of decimal places to
|
|
516
|
+
<4> Number of decimal places to keep; the fraction is truncated, not rounded.
|
|
517
517
|
|
|
518
518
|
Where,
|
|
519
519
|
|
|
@@ -534,7 +534,8 @@ options for number formatting. Use either `localize_number` or
|
|
|
534
534
|
See <<localizer_symbols,format options hash>> for details.
|
|
535
535
|
|
|
536
536
|
`precision: <precision-number>`:: (optional, default `nil`)
|
|
537
|
-
Number of decimal places to
|
|
537
|
+
Number of decimal places to keep; the fraction is truncated, not
|
|
538
|
+
rounded. Accepts an integer value.
|
|
538
539
|
+
|
|
539
540
|
.Specifying a precision of 6 digits
|
|
540
541
|
[example]
|
|
@@ -571,6 +572,28 @@ through a Hash object containing specified options.
|
|
|
571
572
|
|
|
572
573
|
This Hash object is called the "format options Hash".
|
|
573
574
|
|
|
575
|
+
Option values follow a common handling policy:
|
|
576
|
+
|
|
577
|
+
* Count options (`precision`, `significant`, `digit_count`, `group_digits`,
|
|
578
|
+
`fraction_group_digits`, `padding_digits`, `padding_group_digits`) accept an
|
|
579
|
+
`Integer` or its `String`/`Symbol` numeric equivalent (`"4"`). Any other
|
|
580
|
+
value, including negative numbers, raises `Plurimath::ConfigurationError`.
|
|
581
|
+
* Separator options (`decimal`, `group`, `fraction_group`, `base_prefix`,
|
|
582
|
+
`base_postfix`) accept free-form strings. An explicitly passed `nil`
|
|
583
|
+
disables the separator — for `decimal` this renders the digits without a
|
|
584
|
+
decimal mark, and output correctness is then the caller's responsibility.
|
|
585
|
+
One exception: `group: nil` falls back to the default separator instead of
|
|
586
|
+
disabling (use `group: ""` to disable grouping). Boolean values raise
|
|
587
|
+
`Plurimath::ConfigurationError`.
|
|
588
|
+
* Enumerated options (`notation`, `number_sign`, `exponent_sign`, `e`,
|
|
589
|
+
`times`, `hex_capital`) accept their documented values as `Symbol` or
|
|
590
|
+
`String`; unrecognized `String`/`Symbol` values fall back to the default
|
|
591
|
+
behavior. Any other type raises `Plurimath::ConfigurationError` — including
|
|
592
|
+
Boolean, except for `hex_capital`, where `true`/`false` are documented values.
|
|
593
|
+
* The `locale` always falls back to `:en` for unsupported or wrong-typed
|
|
594
|
+
values; it never raises.
|
|
595
|
+
* Invalid number input raises `Plurimath::Errors::InvalidNumber`.
|
|
596
|
+
|
|
574
597
|
Available options are explained below.
|
|
575
598
|
|
|
576
599
|
NOTE: Each option takes an input of a certain specified type. Using an input
|
|
@@ -584,6 +607,15 @@ The same option keys can be passed through `localizer_symbols`, through the
|
|
|
584
607
|
per-call `format:` hash, or through `Plurimath::Formatter::Standard`'s
|
|
585
608
|
`options:` argument.
|
|
586
609
|
|
|
610
|
+
When rendering through `Plurimath::Math::Formula` serialization methods
|
|
611
|
+
(`to_asciimath`, `to_latex`, `to_mathml`, `to_unicodemath`, `to_html`),
|
|
612
|
+
per-element overrides can be supplied via `options[:format]`. The hash is
|
|
613
|
+
forwarded to `NumberFormatter#localized_number`'s `format:` keyword, taking
|
|
614
|
+
precedence over the configured formatter's symbols for that one element.
|
|
615
|
+
Custom formatters with a simpler `localized_number(value)` signature are
|
|
616
|
+
unaffected unless `options[:format]` is supplied, in which case their method
|
|
617
|
+
must accept the `format:` keyword.
|
|
618
|
+
|
|
587
619
|
|
|
588
620
|
`decimal`:: (`String` value)
|
|
589
621
|
Symbol to use for the decimal point. Accepts a character.
|
|
@@ -695,12 +727,12 @@ Missing integer digits are inserted before grouping using the configured
|
|
|
695
727
|
"32" with `padding_group_digits: 4, group: ""` => "0032"
|
|
696
728
|
"32123" with `padding_group_digits: 4, group: ""` => "00032123"
|
|
697
729
|
====
|
|
698
|
-
====
|
|
699
730
|
|
|
700
731
|
`base`:: (`Numeric` value)
|
|
701
732
|
Sets the numeric base (radix) used to render both the integer and fractional parts of the number.
|
|
702
733
|
Supported values are 2 (binary), 8 (octal), 10 (decimal, default) and 16 (hexadecimal).
|
|
703
|
-
|
|
734
|
+
The value is also accepted as its `String` or `Symbol` equivalent (`"16"`).
|
|
735
|
+
Passing any other value for `base` raises `Plurimath::Errors::UnsupportedBase`.
|
|
704
736
|
+
|
|
705
737
|
.Rendering numbers in different bases
|
|
706
738
|
[example]
|
|
@@ -752,6 +784,10 @@ uppercase letters. This includes both hexadecimal digits and any separators
|
|
|
752
784
|
Use `:numbers_only` to uppercase generated hexadecimal digits without changing
|
|
753
785
|
separator characters. It has no effect for other bases.
|
|
754
786
|
+
|
|
787
|
+
Both values are also accepted as their `String` or `Symbol` equivalents
|
|
788
|
+
(`"true"`, `:true`, `"numbers_only"`). Matching is case-sensitive; any other
|
|
789
|
+
value (including any form of `false`) disables uppercasing.
|
|
790
|
+
+
|
|
755
791
|
.Uppercase hexadecimal output
|
|
756
792
|
[example]
|
|
757
793
|
====
|
|
@@ -1027,8 +1063,11 @@ In this example it is `' '` (a space).
|
|
|
1027
1063
|
----
|
|
1028
1064
|
formatter = Plurimath::NumberFormatter.new(:en, localize_number: "#,##0.### ###")
|
|
1029
1065
|
formatter.localized_number("1234.56789")
|
|
1030
|
-
# => "1,234.
|
|
1066
|
+
# => "1,234.567 89"
|
|
1031
1067
|
----
|
|
1068
|
+
|
|
1069
|
+
NOTE: A space used as a group or fraction-group delimiter in the
|
|
1070
|
+
`localize_number` pattern is emitted as U+00A0 NO-BREAK SPACE in the output.
|
|
1032
1071
|
====
|
|
1033
1072
|
|
|
1034
1073
|
|
|
@@ -1052,7 +1091,7 @@ formatter.localized_number(
|
|
|
1052
1091
|
----
|
|
1053
1092
|
<1> The number to be formatted.
|
|
1054
1093
|
<2> The locale to be used for number formatting.
|
|
1055
|
-
<3> The number of decimal places to
|
|
1094
|
+
<3> The number of decimal places to keep; the fraction is truncated, not rounded.
|
|
1056
1095
|
<4> Hash containing the relevant options for number formatting.
|
|
1057
1096
|
|
|
1058
1097
|
Where,
|
|
@@ -1065,8 +1104,8 @@ Value is a symbol.
|
|
|
1065
1104
|
Overrides the locale set during the creation of the `NumberFormatter` object. If
|
|
1066
1105
|
not provided, the locale of the `NumberFormatter` instance will be used.
|
|
1067
1106
|
|
|
1068
|
-
`precision: <precision-number>`:: (optional) The number of decimal places to
|
|
1069
|
-
|
|
1107
|
+
`precision: <precision-number>`:: (optional) The number of decimal places to keep
|
|
1108
|
+
(the fraction is truncated, not rounded). If not provided, the precision of the `NumberFormatter` instance will
|
|
1070
1109
|
be used.
|
|
1071
1110
|
|
|
1072
1111
|
`format: <format-hash>`:: (optional, default `{}`) A Hash containing the relevant
|
|
@@ -1075,7 +1114,8 @@ configuration of the `NumberFormatter`.
|
|
|
1075
1114
|
Takes a Hash in the form of the <<localizer_symbols,format options hash>>.
|
|
1076
1115
|
|
|
1077
1116
|
`precision: <precision-number>`::
|
|
1078
|
-
Number of decimal places to
|
|
1117
|
+
Number of decimal places to keep; the fraction is truncated, not
|
|
1118
|
+
rounded. Accepts an integer value.
|
|
1079
1119
|
+
|
|
1080
1120
|
.Specifying a precision of 6 digits
|
|
1081
1121
|
[example]
|
|
@@ -6,7 +6,8 @@ module Plurimath
|
|
|
6
6
|
DEFAULT_BEHAVIOR = :collect
|
|
7
7
|
|
|
8
8
|
class << self
|
|
9
|
-
def warn(feature:, message: nil, replacement: nil, since: nil,
|
|
9
|
+
def warn(feature:, message: nil, replacement: nil, since: nil,
|
|
10
|
+
remove_in: nil)
|
|
10
11
|
feature = validate_feature(feature)
|
|
11
12
|
return if behavior == :collect && emitted_features[feature]
|
|
12
13
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
class ConfigurationError < Error
|
|
5
|
-
def initialize(type, value: nil, supported: nil)
|
|
5
|
+
def initialize(type, value: nil, supported: nil, option: nil)
|
|
6
6
|
@type = type
|
|
7
7
|
@value = value
|
|
8
8
|
@supported = supported
|
|
9
|
+
@option = option
|
|
9
10
|
super(message)
|
|
10
11
|
end
|
|
11
12
|
|
|
@@ -19,6 +20,9 @@ module Plurimath
|
|
|
19
20
|
when :conflicting_formatter_options
|
|
20
21
|
"formatter options cannot be used together: choose either " \
|
|
21
22
|
":padding_digits or :padding_group_digits"
|
|
23
|
+
when :invalid_formatter_option
|
|
24
|
+
"invalid value #{@value.inspect} for formatter option " \
|
|
25
|
+
"#{@option.inspect}#{" (expected #{@supported})" if @supported}"
|
|
22
26
|
else
|
|
23
27
|
"invalid Plurimath configuration"
|
|
24
28
|
end
|
|
@@ -6,7 +6,8 @@ module Plurimath
|
|
|
6
6
|
|
|
7
7
|
attr_reader :feature, :replacement, :since, :remove_in
|
|
8
8
|
|
|
9
|
-
def initialize(feature:, message: nil, replacement: nil, since: nil,
|
|
9
|
+
def initialize(feature:, message: nil, replacement: nil, since: nil,
|
|
10
|
+
remove_in: nil)
|
|
10
11
|
@feature = feature
|
|
11
12
|
@replacement = replacement
|
|
12
13
|
@since = since
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Errors
|
|
5
|
+
class InvalidNumber < Plurimath::Error
|
|
6
|
+
def initialize(value)
|
|
7
|
+
@value = value
|
|
8
|
+
super(message)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"[plurimath] Invalid number #{@value.inspect} for number formatting. " \
|
|
13
|
+
"Expected a numeric string such as \"1234\", \"-12.34\", or \"1.2e5\"."
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Errors
|
|
5
|
+
class UnsupportedBase < Plurimath::Error
|
|
6
|
+
def initialize(base, supported_bases)
|
|
7
|
+
@base = base
|
|
8
|
+
@supported = supported_bases.keys.join(", ")
|
|
9
|
+
super(message)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def message
|
|
13
|
+
"[plurimath] Unsupported base `#{@base}` for number formatting. " \
|
|
14
|
+
"[plurimath] The formatter `:base` option must be one of: #{@supported}."
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/plurimath/errors.rb
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Errors
|
|
5
|
+
autoload :InvalidNumber, "#{__dir__}/errors/invalid_number"
|
|
6
|
+
autoload :UnsupportedBase, "#{__dir__}/errors/unsupported_base"
|
|
7
|
+
autoload :UnsupportedLocale, "#{__dir__}/errors/unsupported_locale"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -22,7 +22,12 @@ module Plurimath
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def apply(string)
|
|
25
|
-
rendered = upcase_hex?
|
|
25
|
+
rendered = if upcase_hex?
|
|
26
|
+
string.tr(Base::HEX_DIGITS,
|
|
27
|
+
Base::HEX_DIGITS.upcase)
|
|
28
|
+
else
|
|
29
|
+
string
|
|
30
|
+
end
|
|
26
31
|
return rendered if default?
|
|
27
32
|
|
|
28
33
|
"#{base_prefix}#{rendered}#{base_postfix}"
|
|
@@ -59,7 +64,7 @@ module Plurimath
|
|
|
59
64
|
def validate_base!
|
|
60
65
|
return if self.class.supported?(base)
|
|
61
66
|
|
|
62
|
-
raise UnsupportedBase.new(base, DEFAULT_PREFIXES)
|
|
67
|
+
raise Plurimath::Errors::UnsupportedBase.new(base, DEFAULT_PREFIXES)
|
|
63
68
|
end
|
|
64
69
|
end
|
|
65
70
|
end
|
|
@@ -32,11 +32,16 @@ module Plurimath
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def base
|
|
35
|
-
symbols[:base]
|
|
35
|
+
value = symbols[:base]
|
|
36
|
+
return Base::DEFAULT_BASE if value.nil?
|
|
37
|
+
|
|
38
|
+
# Numeric String/Symbol forms are normalized; anything else is left
|
|
39
|
+
# raw so BaseNotation reports it through UnsupportedBase.
|
|
40
|
+
coerce_integer(value) { return value }
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
def base_prefix
|
|
39
|
-
|
|
44
|
+
separator_option(:base_prefix).to_s
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
def base_prefix?
|
|
@@ -44,7 +49,7 @@ module Plurimath
|
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def base_postfix
|
|
47
|
-
|
|
52
|
+
separator_option(:base_postfix)
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
def base_postfix?
|
|
@@ -52,56 +57,84 @@ module Plurimath
|
|
|
52
57
|
end
|
|
53
58
|
|
|
54
59
|
def decimal
|
|
55
|
-
|
|
60
|
+
# An explicitly passed nil renders without a separator; output
|
|
61
|
+
# correctness is then the caller's responsibility.
|
|
62
|
+
separator_option(:decimal, default: DEFAULT_DECIMAL)
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def digit_count
|
|
59
|
-
|
|
66
|
+
integer_option(:digit_count, default: 0)
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
def fraction_group
|
|
63
|
-
|
|
70
|
+
separator_option(:fraction_group).to_s
|
|
64
71
|
end
|
|
65
72
|
|
|
66
73
|
def fraction_group_digits
|
|
67
|
-
|
|
74
|
+
integer_option(:fraction_group_digits)
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
def group
|
|
71
|
-
|
|
78
|
+
# Unlike decimal, an explicit nil group falls back to the default
|
|
79
|
+
# separator; use "" to disable grouping.
|
|
80
|
+
(separator_option(:group,
|
|
81
|
+
default: DEFAULT_GROUP) || DEFAULT_GROUP).to_s
|
|
72
82
|
end
|
|
73
83
|
|
|
74
84
|
def group_digits
|
|
75
|
-
|
|
85
|
+
integer_option(:group_digits, default: DEFAULT_GROUP_DIGITS)
|
|
76
86
|
end
|
|
77
87
|
|
|
78
88
|
def hex_capital
|
|
79
|
-
symbols[:hex_capital]
|
|
89
|
+
value = symbols[:hex_capital]
|
|
90
|
+
# nil is handled first: under Opal `when nil` spuriously matches
|
|
91
|
+
# non-nil values (nil === 1.5 is true there), so it must not appear
|
|
92
|
+
# in the type whitelist below.
|
|
93
|
+
return if value.nil?
|
|
94
|
+
|
|
95
|
+
# Accept only Boolean/String/Symbol and reject any other type.
|
|
96
|
+
# Attribute-parsing callers deliver String/Symbol (:true,
|
|
97
|
+
# "numbers_only"), so the accepted forms normalize through to_s.
|
|
98
|
+
case value
|
|
99
|
+
when true, false, String, Symbol
|
|
100
|
+
case value.to_s
|
|
101
|
+
when "true" then true
|
|
102
|
+
when "numbers_only" then :numbers_only
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
invalid_option!(:hex_capital, value)
|
|
106
|
+
end
|
|
80
107
|
end
|
|
81
108
|
|
|
82
109
|
def number_sign
|
|
83
|
-
|
|
110
|
+
symbol_option(:number_sign)
|
|
84
111
|
end
|
|
85
112
|
|
|
86
113
|
def padding
|
|
87
|
-
value =
|
|
114
|
+
value = separator_option(:padding, default: DEFAULT_PADDING).to_s
|
|
88
115
|
value.empty? ? DEFAULT_PADDING : value[0]
|
|
89
116
|
end
|
|
90
117
|
|
|
91
118
|
def padding_digits
|
|
92
|
-
|
|
119
|
+
integer_option(:padding_digits, default: 0)
|
|
93
120
|
end
|
|
94
121
|
|
|
95
122
|
def padding_group_digits
|
|
96
|
-
|
|
123
|
+
integer_option(:padding_group_digits, default: 0)
|
|
97
124
|
end
|
|
98
125
|
|
|
99
126
|
def notation_supported?
|
|
100
127
|
NotationRenderer.supported?(notation)
|
|
101
128
|
end
|
|
102
129
|
|
|
130
|
+
# Whether precision came from the caller (kwarg or symbols) rather
|
|
131
|
+
# than being inferred from the source by the resolver.
|
|
132
|
+
def explicit_precision?
|
|
133
|
+
@explicit_precision
|
|
134
|
+
end
|
|
135
|
+
|
|
103
136
|
def significant
|
|
104
|
-
|
|
137
|
+
integer_option(:significant, default: 0)
|
|
105
138
|
end
|
|
106
139
|
|
|
107
140
|
def to_h
|
|
@@ -111,7 +144,19 @@ module Plurimath
|
|
|
111
144
|
private
|
|
112
145
|
|
|
113
146
|
def resolve_precision(source, precision, precision_resolver)
|
|
114
|
-
|
|
147
|
+
# A nil check (not ||) so that explicit false reaches validation.
|
|
148
|
+
effective_precision = precision.nil? ? symbols[:precision] : precision
|
|
149
|
+
unless effective_precision.nil?
|
|
150
|
+
value = effective_precision
|
|
151
|
+
effective_precision = coerce_integer(value) do
|
|
152
|
+
invalid_option!(:precision, value)
|
|
153
|
+
end
|
|
154
|
+
if effective_precision.negative?
|
|
155
|
+
invalid_option!(:precision, value,
|
|
156
|
+
expected: "a non-negative integer")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
@explicit_precision = !effective_precision.nil?
|
|
115
160
|
return effective_precision unless precision_resolver
|
|
116
161
|
|
|
117
162
|
precision_resolver.resolve(
|
|
@@ -119,12 +164,73 @@ module Plurimath
|
|
|
119
164
|
precision: effective_precision,
|
|
120
165
|
base: base,
|
|
121
166
|
significant: significant,
|
|
167
|
+
digit_count: digit_count,
|
|
122
168
|
notation_supported: notation_supported?,
|
|
123
169
|
)
|
|
124
170
|
end
|
|
125
171
|
|
|
172
|
+
# Counts must be non-negative Integers; numeric String/Symbol forms are
|
|
173
|
+
# normalized because attribute-parsing callers deliver those.
|
|
174
|
+
def integer_option(key, default: nil)
|
|
175
|
+
value = symbols[key]
|
|
176
|
+
return default if value.nil?
|
|
177
|
+
|
|
178
|
+
integer = coerce_integer(value) { invalid_option!(key, value) }
|
|
179
|
+
if integer.negative?
|
|
180
|
+
invalid_option!(key, value,
|
|
181
|
+
expected: "a non-negative integer")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
integer
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def coerce_integer(value)
|
|
188
|
+
# Note on Opal: JS has a single number type, so a whole-valued Float
|
|
189
|
+
# (4.0) is indistinguishable from the Integer 4 (same value, same
|
|
190
|
+
# to_s "4") — under Opal it is treated as that integer. MRI rejects
|
|
191
|
+
# Floats here; non-whole Floats (1.5) are rejected in both runtimes.
|
|
192
|
+
case value
|
|
193
|
+
when ::Integer then value
|
|
194
|
+
when true, false, Float then yield
|
|
195
|
+
else
|
|
196
|
+
begin
|
|
197
|
+
Integer(value.to_s, 10)
|
|
198
|
+
rescue ArgumentError, TypeError
|
|
199
|
+
yield
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Separators are free-form strings; explicit nil disables the
|
|
205
|
+
# separator, and Booleans are always a caller bug.
|
|
206
|
+
def separator_option(key, default: nil)
|
|
207
|
+
value = symbols.fetch(key, default)
|
|
208
|
+
invalid_option!(key, value) if [true, false].include?(value)
|
|
209
|
+
|
|
210
|
+
value
|
|
211
|
+
end
|
|
212
|
+
|
|
126
213
|
def symbol_option(key)
|
|
127
|
-
symbols[key]
|
|
214
|
+
value = symbols[key]
|
|
215
|
+
return if value.nil?
|
|
216
|
+
|
|
217
|
+
# Enumerated/symbol options accept only String or Symbol; reject
|
|
218
|
+
# other types (and booleans) instead of coercing arbitrary input.
|
|
219
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
|
220
|
+
invalid_option!(key,
|
|
221
|
+
value)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
value.to_sym
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def invalid_option!(key, value, expected: nil)
|
|
228
|
+
raise Plurimath::ConfigurationError.new(
|
|
229
|
+
:invalid_formatter_option,
|
|
230
|
+
option: key,
|
|
231
|
+
value: value,
|
|
232
|
+
supported: expected,
|
|
233
|
+
)
|
|
128
234
|
end
|
|
129
235
|
|
|
130
236
|
def validate_padding_options!
|
|
@@ -28,9 +28,12 @@ module Plurimath
|
|
|
28
28
|
@integer_digits = parts.integer_digits
|
|
29
29
|
|
|
30
30
|
fraction = parts.fraction_digits
|
|
31
|
-
|
|
31
|
+
if !base_default? && fraction.match?(/[1-9]/)
|
|
32
|
+
fraction = change_base(fraction,
|
|
33
|
+
precision)
|
|
34
|
+
end
|
|
32
35
|
number = if @digit_count.positive?
|
|
33
|
-
digit_count_format(fraction
|
|
36
|
+
digit_count_format(fraction)
|
|
34
37
|
else
|
|
35
38
|
format(fraction, precision)
|
|
36
39
|
end
|
|
@@ -67,7 +70,7 @@ module Plurimath
|
|
|
67
70
|
|
|
68
71
|
# The digit_count option is a total visible-digit budget, so fraction
|
|
69
72
|
# rounding can carry back into the integer digits.
|
|
70
|
-
def digit_count_format(fraction
|
|
73
|
+
def digit_count_format(fraction)
|
|
71
74
|
integer = integer_digits + DEFAULT_STRINGS[:dot] + fraction
|
|
72
75
|
int_length = integer.length.pred # integer length; excluding the decimal point
|
|
73
76
|
if int_length > @digit_count
|
|
@@ -79,24 +82,12 @@ module Plurimath
|
|
|
79
82
|
end
|
|
80
83
|
round_base_string(fraction)
|
|
81
84
|
elsif int_length < @digit_count
|
|
82
|
-
fraction + (DEFAULT_STRINGS[:zero] * (
|
|
85
|
+
fraction + (DEFAULT_STRINGS[:zero] * (@digit_count - int_length))
|
|
83
86
|
else
|
|
84
87
|
fraction
|
|
85
88
|
end
|
|
86
89
|
end
|
|
87
90
|
|
|
88
|
-
def update_digit_count(number, precision)
|
|
89
|
-
return @digit_count unless zeros_count_in(number) == precision
|
|
90
|
-
|
|
91
|
-
@digit_count - precision + 1
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def zeros_count_in(number)
|
|
95
|
-
return unless number.chars.all?(DEFAULT_STRINGS[:zero])
|
|
96
|
-
|
|
97
|
-
number.length
|
|
98
|
-
end
|
|
99
|
-
|
|
100
91
|
def round_base_string(fraction)
|
|
101
92
|
digits = fraction[0..frac_digit_count].chars
|
|
102
93
|
discard_char = digits.pop
|
|
@@ -24,6 +24,9 @@ module Plurimath
|
|
|
24
24
|
def format_groups(string)
|
|
25
25
|
string = capitalize_hex_digits(string)
|
|
26
26
|
string = pad_integer(string)
|
|
27
|
+
# group_digits: 0 disables grouping, mirroring fraction grouping.
|
|
28
|
+
return string unless groups.positive?
|
|
29
|
+
|
|
27
30
|
tokens = []
|
|
28
31
|
|
|
29
32
|
until string.empty?
|
|
@@ -44,7 +44,11 @@ module Plurimath
|
|
|
44
44
|
def render_engineering(source)
|
|
45
45
|
parts = notation_parts(source)
|
|
46
46
|
parts = engineering_notation_parts(parts) unless source.decimal.zero?
|
|
47
|
-
parts[0] = localize_parts(
|
|
47
|
+
parts[0] = localize_parts(
|
|
48
|
+
source,
|
|
49
|
+
parts[0],
|
|
50
|
+
precision: engineering_precision(source, parts[0]),
|
|
51
|
+
)
|
|
48
52
|
parts.join(" #{options.times} 10^")
|
|
49
53
|
end
|
|
50
54
|
|
|
@@ -66,7 +70,10 @@ module Plurimath
|
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def notation_parts(source)
|
|
69
|
-
|
|
73
|
+
if source.decimal.zero?
|
|
74
|
+
return [source.to_parts(base: options.base),
|
|
75
|
+
0]
|
|
76
|
+
end
|
|
70
77
|
|
|
71
78
|
parts = source.to_parts(base: Base::DEFAULT_BASE)
|
|
72
79
|
digits, exponent = significant_digits_and_exponent(parts)
|
|
@@ -87,7 +94,9 @@ module Plurimath
|
|
|
87
94
|
exponent -= index
|
|
88
95
|
digits = "#{coefficient.integer_digits}#{coefficient.fraction_digits}"
|
|
89
96
|
integer_length = index + 1
|
|
90
|
-
integer_digits = digits[0...integer_length].to_s.ljust(
|
|
97
|
+
integer_digits = digits[0...integer_length].to_s.ljust(
|
|
98
|
+
integer_length, "0"
|
|
99
|
+
)
|
|
91
100
|
|
|
92
101
|
[
|
|
93
102
|
Parts.new(
|
|
@@ -100,10 +109,30 @@ module Plurimath
|
|
|
100
109
|
]
|
|
101
110
|
end
|
|
102
111
|
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
# The inferred budget covers the source's significant digits; the
|
|
113
|
+
# engineering shift moves one to three of them into the integer part
|
|
114
|
+
# (per exponent mod 3), so the fraction budget must subtract the
|
|
115
|
+
# shifted integer width. Only a positive explicit precision is taken
|
|
116
|
+
# as a literal fraction width; precision: 0 falls through to inference.
|
|
117
|
+
def engineering_precision(source, coefficient)
|
|
118
|
+
# precision: 0 is intentionally not literal here; it infers.
|
|
119
|
+
return precision if options.explicit_precision? && precision.positive?
|
|
120
|
+
# Zero sources carry their stated fraction width; an explicit
|
|
121
|
+
# precision: 0 still infers (consistent with non-zero engineering).
|
|
122
|
+
return source.notation_precision if source.decimal.zero?
|
|
123
|
+
|
|
124
|
+
integer_length = coefficient.integer_digits.length
|
|
125
|
+
budget = [options.significant, options.digit_count].max
|
|
126
|
+
unless budget.positive?
|
|
127
|
+
return [source.significant_digit_count - integer_length,
|
|
128
|
+
0].max
|
|
129
|
+
end
|
|
105
130
|
|
|
106
|
-
[
|
|
131
|
+
fraction_budget = [budget - integer_length, 0].max
|
|
132
|
+
# Leave one digit for Significant's rounding pass when the source
|
|
133
|
+
# carries more digits than the requested budget.
|
|
134
|
+
fraction_budget += 1 if source.significant_digit_count > budget
|
|
135
|
+
fraction_budget
|
|
107
136
|
end
|
|
108
137
|
|
|
109
138
|
def significant_digits_and_exponent(parts)
|
|
@@ -26,15 +26,18 @@ module Plurimath
|
|
|
26
26
|
render_precision = precision || options.precision || source_precision
|
|
27
27
|
parts = source.to_parts(
|
|
28
28
|
base: options.base,
|
|
29
|
-
precision: render_precision,
|
|
29
|
+
precision: decimal_precision_for(render_precision),
|
|
30
30
|
)
|
|
31
31
|
format_parts(parts, precision: render_precision)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def format_parts(parts, precision:)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
decimal_precision = decimal_precision_for(precision)
|
|
36
|
+
unless decimal_precision.nil?
|
|
37
|
+
parts = parts.with_digits(
|
|
38
|
+
fraction_digits: parts.fraction_digits[0...decimal_precision.to_i].to_s,
|
|
39
|
+
)
|
|
40
|
+
end
|
|
38
41
|
parts = renderable_parts(parts, precision: precision)
|
|
39
42
|
|
|
40
43
|
parts = significant_format.apply_parts(parts) if significant_format.active?
|
|
@@ -60,6 +63,15 @@ module Plurimath
|
|
|
60
63
|
|
|
61
64
|
source.fraction_digits.length
|
|
62
65
|
end
|
|
66
|
+
|
|
67
|
+
# Precision budgets target-base digits, so the decimal fraction must
|
|
68
|
+
# stay intact until Fraction#change_base generates them; truncating
|
|
69
|
+
# decimal digits first loses value (0.07 in hex became 0x0.0).
|
|
70
|
+
def decimal_precision_for(precision)
|
|
71
|
+
return precision if options.base == Base::DEFAULT_BASE
|
|
72
|
+
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
63
75
|
end
|
|
64
76
|
end
|
|
65
77
|
end
|