plurimath 0.9.11 → 0.10.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/.github/workflows/release.yml +3 -0
- data/README.adoc +131 -11
- data/lib/plurimath/asciimath.rb +0 -1
- data/lib/plurimath/cli.rb +1 -1
- data/lib/plurimath/errors/formatter/unsupported_base.rb +19 -0
- data/lib/plurimath/errors/invalid_type_error.rb +19 -0
- data/lib/plurimath/errors/parse_error.rb +39 -0
- data/lib/plurimath/errors.rb +5 -0
- data/lib/plurimath/formatter/number_formatter.rb +52 -10
- data/lib/plurimath/formatter/numbers/base.rb +37 -0
- data/lib/plurimath/formatter/numbers/fraction.rb +146 -35
- data/lib/plurimath/formatter/numbers/integer.rb +16 -12
- data/lib/plurimath/formatter/numbers/significant.rb +84 -52
- data/lib/plurimath/formatter.rb +2 -0
- data/lib/plurimath/math/core.rb +1 -0
- data/lib/plurimath/math/formula.rb +10 -5
- data/lib/plurimath/math/function/font_style.rb +68 -5
- data/lib/plurimath/math.rb +7 -25
- data/lib/plurimath/mathml.rb +0 -2
- data/lib/plurimath/unitsml.rb +4 -11
- data/lib/plurimath/version.rb +1 -1
- data/plurimath.gemspec +1 -0
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e78c38692b0343fbe7666f309339f0e94180803b8aa81e48c71897d37eab606
|
|
4
|
+
data.tar.gz: 44c7c9ae6c2fb5d9b09d869c09c91775172f77b9bf27dee0aeb511f5bb272046
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d79d19001092a56d19ae94705c3777460fd24cd57ebca84b1a6ee1d9b68ed473e0cf3799d45c58ba6af6f3aa0e56dc8b8160559bc43c70404885adb024a2232
|
|
7
|
+
data.tar.gz: c256d08993caa5a78caded66b7aaa65c8c7a52f413bb14dacfafbe29f2984fa604516942a8b28b5684486f34eb99ca3da8f03dc5f11513a41381484e0a507d7d
|
data/README.adoc
CHANGED
|
@@ -129,7 +129,7 @@ plurimath convert -i "equation" -f omml -t mathml -d true
|
|
|
129
129
|
====
|
|
130
130
|
[source,bash]
|
|
131
131
|
----
|
|
132
|
-
plurimath convert -
|
|
132
|
+
plurimath convert -p <file_path> -f asciimath -t unicodemath
|
|
133
133
|
----
|
|
134
134
|
====
|
|
135
135
|
|
|
@@ -524,14 +524,25 @@ Symbol to use for the decimal point. Accepts a character.
|
|
|
524
524
|
====
|
|
525
525
|
|
|
526
526
|
`digit_count`:: (`Numeric` value)
|
|
527
|
-
Total number of digits to render, with the value truncated
|
|
528
|
-
Accepts an integer value.
|
|
527
|
+
Total number of digits to render (integer + fractional), with the value truncated
|
|
528
|
+
or rounded as necessary. Accepts an integer value.
|
|
529
|
+
+
|
|
530
|
+
When `digit_count` is less than or equal to the integer length, the fractional
|
|
531
|
+
part is omitted entirely, and the integer is rounded based on whether the
|
|
532
|
+
fractional value is >= 0.5 (or >= base/2 for non-decimal bases).
|
|
529
533
|
+
|
|
530
534
|
.Specifying a total of 6 digits in rendering the number
|
|
531
535
|
[example]
|
|
532
536
|
====
|
|
533
537
|
"32232.232" => "32232.2"
|
|
534
538
|
====
|
|
539
|
+
+
|
|
540
|
+
.When digit_count is smaller than the integer length
|
|
541
|
+
[example]
|
|
542
|
+
====
|
|
543
|
+
"99999.999" with `digit_count: 3` => "100,000" (fractional part omitted, integer rounded up)
|
|
544
|
+
"12345.123" with `digit_count: 3` => "12,345" (fractional part omitted, no rounding)
|
|
545
|
+
====
|
|
535
546
|
|
|
536
547
|
|
|
537
548
|
`group`:: (`String` value)
|
|
@@ -555,6 +566,116 @@ Accepts an integer value. (default is 3 in most locales.)
|
|
|
555
566
|
"32232.232" => "3 22 32.232"
|
|
556
567
|
====
|
|
557
568
|
|
|
569
|
+
`base`:: (`Numeric` value)
|
|
570
|
+
Sets the numeric base (radix) used to render both the integer and fractional parts of the number.
|
|
571
|
+
Supported values are 2 (binary), 8 (octal), 10 (decimal, default) and 16 (hexadecimal).
|
|
572
|
+
Passing any other value for `base` raises `Plurimath::Formatter::UnsupportedBase`.
|
|
573
|
+
+
|
|
574
|
+
.Rendering numbers in different bases
|
|
575
|
+
[example]
|
|
576
|
+
====
|
|
577
|
+
"10" with `base: 2, group_digits: 2, group: ","` => "0b10,10"
|
|
578
|
+
"9" with `base: 8` => "0o11"
|
|
579
|
+
"255" with `base: 16` => "0xff"
|
|
580
|
+
"10.75" with `base: 2, group_digits: 2, group: ","` => "0b10,10.11" (integer and fractional parts converted, with grouping)
|
|
581
|
+
====
|
|
582
|
+
|
|
583
|
+
`base_prefix`:: (`String` or `nil` value)
|
|
584
|
+
Overrides the default base prefix for non‑decimal bases.
|
|
585
|
+
When omitted, standard prefixes are used (`0b` for base 2, `0o` for base 8, `0x` for base 16).
|
|
586
|
+
`nil` or an empty string can be used to omit the prefix.
|
|
587
|
+
+
|
|
588
|
+
.Custom base prefixes
|
|
589
|
+
[example]
|
|
590
|
+
====
|
|
591
|
+
"255" with `base: 16, base_prefix: "16#"` => "16#ff"
|
|
592
|
+
"255" with `base: 16, base_prefix: nil` => "ff"
|
|
593
|
+
"255" with `base: 16, base_prefix: ""` => "ff"
|
|
594
|
+
====
|
|
595
|
+
|
|
596
|
+
`base_postfix`:: (`String` value)
|
|
597
|
+
If present, a postfix is appended to the converted number instead of using any base prefix.
|
|
598
|
+
This is applied after digit grouping.
|
|
599
|
+
`nil` or an empty string can be used to omit the postfix.
|
|
600
|
+
+
|
|
601
|
+
.Using postfix notation
|
|
602
|
+
[example]
|
|
603
|
+
====
|
|
604
|
+
"255" with `base: 16, base_postfix: "_16"` => "ff_16"
|
|
605
|
+
"255" with `base: 16, base_postfix: nil` => "ff"
|
|
606
|
+
"255" with `base: 16, base_postfix: ""` => "ff"
|
|
607
|
+
====
|
|
608
|
+
|
|
609
|
+
`hex_capital`:: (`Boolean` value)
|
|
610
|
+
When `true` and `base: 16`, all characters in the range `a`-`f` in the converted output
|
|
611
|
+
are rendered using uppercase letters. This includes both hexadecimal digits and any
|
|
612
|
+
separators (such as `decimal`, `group`, `fraction_group`) that happen to be letters
|
|
613
|
+
in the `a`-`f` range. The `base_prefix` and `base_postfix` values are never modified.
|
|
614
|
+
It has no effect for other bases.
|
|
615
|
+
+
|
|
616
|
+
.Uppercase hexadecimal output
|
|
617
|
+
[example]
|
|
618
|
+
====
|
|
619
|
+
"48879" with `base: 16, hex_capital: true` => "0xBE,EF"
|
|
620
|
+
====
|
|
621
|
+
+
|
|
622
|
+
WARNING: If you use separator characters in the range `a`-`f` (such as `decimal: "d"` or `fraction_group: "f"`), those separators will also be uppercased when `hex_capital: true`. Choose separators outside this range (e.g., `".", ",", " ", "_"`) if you need them to remain unchanged.
|
|
623
|
+
+
|
|
624
|
+
.Separator uppercasing behavior
|
|
625
|
+
[example]
|
|
626
|
+
====
|
|
627
|
+
"48879" with `base: 16, hex_capital: true, group: "g"` => "0xBEgEF" (g remains lowercase)
|
|
628
|
+
|
|
629
|
+
"48879" with `base: 16, hex_capital: true, group: "f"` => "0xBEFEF" (f separator becomes F)
|
|
630
|
+
====
|
|
631
|
+
|
|
632
|
+
NOTE: When `base` is not 10, both the integer and fractional parts are converted to the specified base.
|
|
633
|
+
The decimal separator is rendered as configured by the `decimal` option.
|
|
634
|
+
+
|
|
635
|
+
NOTE: The fractional-part conversion treats the digits after the decimal point as a fractional value (for example, `"10.75"` is interpreted
|
|
636
|
+
as decimal `0.75` for the fractional part) and converts that real value to the requested base, honoring the configured precision.
|
|
637
|
+
|
|
638
|
+
===== Base conversion with other formatting options
|
|
639
|
+
|
|
640
|
+
Base conversion works seamlessly with other formatting options such as `precision`, `digit_count`,
|
|
641
|
+
`fraction_group`, `fraction_group_digits`, and `significant`.
|
|
642
|
+
|
|
643
|
+
.Base conversion with precision
|
|
644
|
+
[example]
|
|
645
|
+
====
|
|
646
|
+
Controls the maximum number of fractional digits in the converted base (pads with zeros when needed and truncates repeating expansions to the configured precision):
|
|
647
|
+
+
|
|
648
|
+
"10.75" with `base: 2, precision: 4` => "0b10,10.1100" (fractional part padded to 4 digits in base 2)
|
|
649
|
+
"0.5" with `base: 16, precision: 6` => "0x0.800000" (fractional part converted to base 16 and padded to 6 digits)
|
|
650
|
+
====
|
|
651
|
+
|
|
652
|
+
.Base conversion with fraction grouping
|
|
653
|
+
[example]
|
|
654
|
+
====
|
|
655
|
+
Groups fractional digits in the converted base:
|
|
656
|
+
+
|
|
657
|
+
"10.75" with `base: 2, fraction_group_digits: 1, fraction_group: " ", group_digits: 10` => "0b1010.1 1"
|
|
658
|
+
====
|
|
659
|
+
|
|
660
|
+
.Base conversion with digit_count
|
|
661
|
+
[example]
|
|
662
|
+
====
|
|
663
|
+
Limits total digits when using base conversion:
|
|
664
|
+
+
|
|
665
|
+
"14236.39239" with `base: 10, digit_count: 6, group_digits: 3` => "14,236.4"
|
|
666
|
+
"10.75" with `base: 2, digit_count: 7, group_digits: 10` => "0b1010.110"
|
|
667
|
+
"255.9" with `base: 16, digit_count: 2, group_digits: 10` => "0x100" (fractional part omitted, integer rounded)
|
|
668
|
+
====
|
|
669
|
+
|
|
670
|
+
.Base conversion with significant digits
|
|
671
|
+
[example]
|
|
672
|
+
====
|
|
673
|
+
Applies significant digit rounding with base conversion:
|
|
674
|
+
+
|
|
675
|
+
"1234.56" with `base: 10, significant: 4` => "1,235"
|
|
676
|
+
"1999" with `base: 10, significant: 2` => "2,000"
|
|
677
|
+
====
|
|
678
|
+
|
|
558
679
|
`fraction_group`:: (`String` value)
|
|
559
680
|
Delimiter to use between groups of fractional digits specified in
|
|
560
681
|
`fraction_group_digits`. Accepts a character.
|
|
@@ -607,7 +728,7 @@ selected to be divisible by three to match the common metric prefixes.
|
|
|
607
728
|
====
|
|
608
729
|
|
|
609
730
|
`e`:: (`String` value)
|
|
610
|
-
Symbol to use for exponents in E notation (default value `
|
|
731
|
+
Symbol to use for exponents in E notation (default value `e`). (used in the
|
|
611
732
|
mode: `e` only).
|
|
612
733
|
+
|
|
613
734
|
.Using the lowercase 'e' symbol as the exponent symbol
|
|
@@ -743,9 +864,8 @@ formatter.localized_number(
|
|
|
743
864
|
|
|
744
865
|
Where,
|
|
745
866
|
|
|
746
|
-
`<number>`:: (mandatory) The number to be formatted
|
|
747
|
-
|
|
748
|
-
raised.
|
|
867
|
+
`<number>`:: (mandatory) The number to be formatted, as a `String` containing the
|
|
868
|
+
decimal representation of the value (for example, `"1234.56789"`).
|
|
749
869
|
|
|
750
870
|
`locale: <locale-symbol>`:: (optional) The locale to be used for number formatting.
|
|
751
871
|
Value is a symbol.
|
|
@@ -1117,7 +1237,7 @@ locales are listed in the link:/blog/2024-07-09-number-formatter[number formatte
|
|
|
1117
1237
|
`options`:: (default: empty) a hash of options (`localizer_symbols`). The options
|
|
1118
1238
|
are listed in the link:/blog/2024-07-09-number-formatter[number formatter blog post].
|
|
1119
1239
|
|
|
1120
|
-
`
|
|
1240
|
+
`string_format`:: (default: `nil`, disabled) a string value (`localize_number`)
|
|
1121
1241
|
|
|
1122
1242
|
`precision`:: (default: `nil`, disabled) an integer value.
|
|
1123
1243
|
|
|
@@ -1136,7 +1256,7 @@ are listed in the link:/blog/2024-07-09-number-formatter[number formatter blog p
|
|
|
1136
1256
|
}
|
|
1137
1257
|
|
|
1138
1258
|
> formatter = Plurimath::Formatter::Standard.new(locale: :hy, options: options, precision: 2)
|
|
1139
|
-
#
|
|
1259
|
+
# string_format: <string value> if provided
|
|
1140
1260
|
|
|
1141
1261
|
> Plurimath::Math.parse('2121221.3434', :latex).to_latex(formatter: formatter)
|
|
1142
1262
|
# => '2,12,12,21;34'
|
|
@@ -1160,7 +1280,7 @@ The custom formatter is to be subclassed from `Plurimath::Formatter::Standard`.
|
|
|
1160
1280
|
[source,ruby]
|
|
1161
1281
|
----
|
|
1162
1282
|
class MyCustomFormatter < Plurimath::Formatter::Standard <1>
|
|
1163
|
-
def initialize(locale:, precision:, options:,
|
|
1283
|
+
def initialize(locale:, precision:, options:, string_format:) <2>
|
|
1164
1284
|
super
|
|
1165
1285
|
end
|
|
1166
1286
|
end
|
|
@@ -1175,7 +1295,7 @@ The default options of the custom formatter are set using the
|
|
|
1175
1295
|
[source,ruby]
|
|
1176
1296
|
----
|
|
1177
1297
|
class MyCustomFormatter < Plurimath::Formatter::Standard
|
|
1178
|
-
def initialize(locale:, precision:, options:,
|
|
1298
|
+
def initialize(locale:, precision:, options:, string_format:)
|
|
1179
1299
|
super
|
|
1180
1300
|
end
|
|
1181
1301
|
|
data/lib/plurimath/asciimath.rb
CHANGED
data/lib/plurimath/cli.rb
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Formatter
|
|
5
|
+
class UnsupportedBase < StandardError
|
|
6
|
+
def initialize(base, supported_bases)
|
|
7
|
+
@base = base
|
|
8
|
+
@supported = supported_bases.keys.map { |key| key.to_s.inspect }.join(", ")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
<<~MESSAGE
|
|
13
|
+
[plurimath] Unsupported base `#{@base}` for number formatting.
|
|
14
|
+
[plurimath] The formatter `:base` option must be one of: #{@supported}.
|
|
15
|
+
MESSAGE
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Math
|
|
5
|
+
class InvalidTypeError < TypeError
|
|
6
|
+
def initialize(type = nil)
|
|
7
|
+
super(type ? formula_message(type) : parse_message)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def parse_message
|
|
11
|
+
"`type` must be one of: `#{Math::VALID_TYPES.keys.join('`, `')}`"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def formula_message(type)
|
|
15
|
+
"Invalid type provided: #{type}. Must be one of #{Formula::MATH_ZONE_TYPES.join(', ')}."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Math
|
|
5
|
+
class ParseError < StandardError
|
|
6
|
+
def initialize(text, type)
|
|
7
|
+
@text = text
|
|
8
|
+
@type = type.to_sym
|
|
9
|
+
super(@type == :invalid_unitsml ? unitsml_message : parsing_message)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parsing_message
|
|
13
|
+
<<~MESSAGE
|
|
14
|
+
[plurimath] Error: Failed to parse the following formula with type `#{@type}`.
|
|
15
|
+
[plurimath] Please first manually validate the formula.
|
|
16
|
+
#{generic_part}
|
|
17
|
+
---- FORMULA BEGIN ----
|
|
18
|
+
#{@text}
|
|
19
|
+
---- FORMULA END ----
|
|
20
|
+
MESSAGE
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def unitsml_message
|
|
24
|
+
<<~MESSAGE
|
|
25
|
+
[plurimath] Invalid formula `#{@text}`.
|
|
26
|
+
[plurimath] The use of a variable as an exponent is not valid.
|
|
27
|
+
#{generic_part}
|
|
28
|
+
MESSAGE
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def generic_part
|
|
32
|
+
<<~MESSAGE.rstrip
|
|
33
|
+
[plurimath] If this is a bug, please report the formula at our issue tracker at:
|
|
34
|
+
[plurimath] https://github.com/plurimath/plurimath/issues
|
|
35
|
+
MESSAGE
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -3,17 +3,33 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
module Formatter
|
|
5
5
|
class NumberFormatter
|
|
6
|
-
attr_reader :number, :data_reader
|
|
6
|
+
attr_reader :number, :data_reader
|
|
7
7
|
|
|
8
|
+
DEFAULT_BASE = Numbers::Base::DEFAULT_BASE
|
|
9
|
+
HEX_ALPHABETS = "abcdef".freeze
|
|
8
10
|
STRING_SYMBOLS = {
|
|
9
|
-
dot: "."
|
|
10
|
-
f: "F"
|
|
11
|
+
dot: ".",
|
|
12
|
+
f: "F",
|
|
13
|
+
}.freeze
|
|
14
|
+
DEFAULT_BASE_PREFIXES = {
|
|
15
|
+
2 => "0b",
|
|
16
|
+
8 => "0o",
|
|
17
|
+
10 => "",
|
|
18
|
+
16 => "0x",
|
|
11
19
|
}.freeze
|
|
12
20
|
|
|
13
21
|
def initialize(number, data_reader = {})
|
|
14
22
|
@number = number
|
|
15
23
|
@data_reader = data_reader
|
|
16
|
-
@
|
|
24
|
+
@base = data_reader[:base] || DEFAULT_BASE
|
|
25
|
+
raise UnsupportedBase.new(@base, DEFAULT_BASE_PREFIXES) unless DEFAULT_BASE_PREFIXES.key?(@base)
|
|
26
|
+
|
|
27
|
+
# Handle base_prefix: if explicitly provided (even as nil), use it; otherwise use default
|
|
28
|
+
@base_prefix = if data_reader.key?(:base_prefix)
|
|
29
|
+
data_reader[:base_prefix].to_s
|
|
30
|
+
else
|
|
31
|
+
DEFAULT_BASE_PREFIXES[@base]
|
|
32
|
+
end
|
|
17
33
|
end
|
|
18
34
|
|
|
19
35
|
def format(precision: nil)
|
|
@@ -22,16 +38,37 @@ module Plurimath
|
|
|
22
38
|
# FIX FOR:
|
|
23
39
|
# NotImplementedError: String#<< not supported. Mutable String methods are not supported in Opal.
|
|
24
40
|
result = []
|
|
25
|
-
result << integer_format.apply(int
|
|
26
|
-
result << fraction_format.apply(frac,
|
|
41
|
+
result << integer_format.apply(int)
|
|
42
|
+
result << fraction_format.apply(frac, result, integer_format) # use formatted int for correct fraction formatting
|
|
27
43
|
result = result.join
|
|
28
44
|
result = signif_format.apply(result, integer_format, fraction_format)
|
|
29
|
-
result =
|
|
30
|
-
|
|
45
|
+
result = result.tr(HEX_ALPHABETS, HEX_ALPHABETS.upcase) if upcase_hex?
|
|
46
|
+
result = pre_post_fixed(result) unless base_default?
|
|
47
|
+
"#{prefix_symbol}#{result}"
|
|
31
48
|
end
|
|
32
49
|
|
|
33
50
|
private
|
|
34
51
|
|
|
52
|
+
def upcase_hex?
|
|
53
|
+
@base == 16 && data_reader[:hex_capital]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def prefix_symbol
|
|
57
|
+
if number.negative?
|
|
58
|
+
"-"
|
|
59
|
+
elsif data_reader[:number_sign]&.to_sym == :plus
|
|
60
|
+
"+"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def pre_post_fixed(result)
|
|
65
|
+
if data_reader.key?(:base_postfix)
|
|
66
|
+
"#{result}#{data_reader[:base_postfix]}"
|
|
67
|
+
else
|
|
68
|
+
"#{@base_prefix}#{result}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
35
72
|
def partition_tokens(number)
|
|
36
73
|
int, fraction = parse_number(number)
|
|
37
74
|
[
|
|
@@ -53,10 +90,11 @@ module Plurimath
|
|
|
53
90
|
def parse_number(number, options = data_reader)
|
|
54
91
|
precision = options[:precision] || precision_from(number)
|
|
55
92
|
|
|
93
|
+
abs = round_to(number, precision).abs
|
|
56
94
|
num = if precision == 0
|
|
57
|
-
|
|
95
|
+
abs.fix.to_s(STRING_SYMBOLS[:f])
|
|
58
96
|
else
|
|
59
|
-
|
|
97
|
+
abs.round(precision).to_s(STRING_SYMBOLS[:f])
|
|
60
98
|
end
|
|
61
99
|
num.split(STRING_SYMBOLS[:dot])
|
|
62
100
|
end
|
|
@@ -65,6 +103,10 @@ module Plurimath
|
|
|
65
103
|
factor = BigDecimal(10).power(precision)
|
|
66
104
|
(number * factor).fix / factor
|
|
67
105
|
end
|
|
106
|
+
|
|
107
|
+
def base_default?
|
|
108
|
+
@base == DEFAULT_BASE
|
|
109
|
+
end
|
|
68
110
|
end
|
|
69
111
|
end
|
|
70
112
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Formatter
|
|
5
|
+
module Numbers
|
|
6
|
+
class Base
|
|
7
|
+
HEX_ALPHANUMERIC = %w[0 1 2 3 4 5 6 7 8 9 a b c d e f].freeze
|
|
8
|
+
DEFAULT_BASE = 10
|
|
9
|
+
DIGIT_VALUE = HEX_ALPHANUMERIC.each_with_index.to_h
|
|
10
|
+
|
|
11
|
+
attr_accessor :base, :symbols
|
|
12
|
+
|
|
13
|
+
def initialize(symbols = {})
|
|
14
|
+
@symbols = symbols
|
|
15
|
+
@base = symbols[:base] || DEFAULT_BASE
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
def threshold
|
|
21
|
+
@threshold ||= base.div(2)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def base_default?
|
|
25
|
+
base == DEFAULT_BASE
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def next_mapping_char(char)
|
|
29
|
+
current_idx = DIGIT_VALUE[char]
|
|
30
|
+
return nil unless current_idx
|
|
31
|
+
|
|
32
|
+
HEX_ALPHANUMERIC[current_idx + 1]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -3,69 +3,84 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
module Formatter
|
|
5
5
|
module Numbers
|
|
6
|
-
class Fraction
|
|
6
|
+
class Fraction < Base
|
|
7
7
|
attr_reader :decimal, :precision, :separator, :group
|
|
8
8
|
|
|
9
|
+
DEFAULT_PRECISION = 3
|
|
10
|
+
DEFAULT_STRINGS = { empty: "", zero: "0", dot: ".", f: "F" }.freeze
|
|
11
|
+
|
|
9
12
|
def initialize(symbols = {})
|
|
10
|
-
|
|
11
|
-
@
|
|
12
|
-
@
|
|
13
|
-
@
|
|
14
|
-
@
|
|
13
|
+
super
|
|
14
|
+
@group = symbols[:fraction_group_digits]
|
|
15
|
+
@decimal = symbols.fetch(:decimal, DEFAULT_STRINGS[:dot])
|
|
16
|
+
@int_group = symbols[:group]
|
|
17
|
+
@separator = symbols[:fraction_group].to_s
|
|
18
|
+
@precision = symbols.fetch(:precision, DEFAULT_PRECISION)
|
|
19
|
+
@digit_count = symbols[:digit_count].to_i
|
|
15
20
|
end
|
|
16
21
|
|
|
17
|
-
def apply(fraction,
|
|
18
|
-
precision =
|
|
19
|
-
|
|
22
|
+
def apply(fraction, result, integer_formatter)
|
|
23
|
+
precision = symbols[:precision] || @precision
|
|
24
|
+
@result = result
|
|
25
|
+
@integer_formatter = integer_formatter
|
|
26
|
+
return DEFAULT_STRINGS[:empty] unless precision.positive?
|
|
27
|
+
|
|
28
|
+
fraction = change_base(fraction) if !base_default? && fraction.match?(/[1-9]/)
|
|
20
29
|
|
|
21
|
-
number = if @digit_count
|
|
22
|
-
digit_count_format(
|
|
30
|
+
number = if @digit_count.positive?
|
|
31
|
+
digit_count_format(fraction)
|
|
23
32
|
else
|
|
24
33
|
format(fraction, precision)
|
|
25
34
|
end
|
|
26
|
-
|
|
27
|
-
formatted_number
|
|
28
|
-
formatted_number ? decimal + formatted_number : ""
|
|
35
|
+
formatted_number = format_groups(number) if number && !number.empty?
|
|
36
|
+
formatted_number ? decimal + formatted_number : DEFAULT_STRINGS[:empty]
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
def format(number, precision)
|
|
32
|
-
number
|
|
40
|
+
return number if precision <= number.length
|
|
41
|
+
|
|
42
|
+
number + (DEFAULT_STRINGS[:zero] * (precision - number.length))
|
|
33
43
|
end
|
|
34
44
|
|
|
35
|
-
def format_groups(string)
|
|
36
|
-
|
|
45
|
+
def format_groups(string, length = group)
|
|
46
|
+
length = string.length if group.to_i.zero?
|
|
47
|
+
|
|
48
|
+
change_format(string, length)
|
|
37
49
|
end
|
|
38
50
|
|
|
39
51
|
protected
|
|
40
52
|
|
|
41
|
-
def change_format(string)
|
|
53
|
+
def change_format(string, length)
|
|
42
54
|
tokens = []
|
|
43
|
-
|
|
55
|
+
until string.empty?
|
|
56
|
+
tokens << string.slice(0, length)
|
|
57
|
+
string = string[tokens.last.size..-1]
|
|
58
|
+
end
|
|
44
59
|
tokens.compact.join(separator)
|
|
45
60
|
end
|
|
46
61
|
|
|
47
|
-
def digit_count_format(
|
|
48
|
-
integer =
|
|
49
|
-
int_length = integer.length
|
|
50
|
-
@digit_count ||= int_length
|
|
62
|
+
def digit_count_format(fraction)
|
|
63
|
+
integer = raw_integer + DEFAULT_STRINGS[:dot] + fraction
|
|
64
|
+
int_length = integer.length.pred # integer length; excluding the decimal point
|
|
51
65
|
if int_length > @digit_count
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
# When digit_count is less than or equal to the integer length,
|
|
67
|
+
# omit the fractional part entirely and handle rounding in the integer
|
|
68
|
+
if @digit_count <= raw_integer.length
|
|
69
|
+
# Check if we need to round the integer up based on the fractional part
|
|
70
|
+
if fraction.chars.first && DIGIT_VALUE[fraction.chars.first] >= threshold
|
|
71
|
+
# Round up the integer part
|
|
72
|
+
round_integer([], 1)
|
|
73
|
+
end
|
|
74
|
+
return DEFAULT_STRINGS[:empty]
|
|
75
|
+
end
|
|
76
|
+
round_base_string(fraction)
|
|
54
77
|
elsif int_length < @digit_count
|
|
55
|
-
fraction + (
|
|
78
|
+
fraction + (DEFAULT_STRINGS[:zero] * (update_digit_count(fraction) - int_length))
|
|
56
79
|
else
|
|
57
80
|
fraction
|
|
58
81
|
end
|
|
59
82
|
end
|
|
60
83
|
|
|
61
|
-
def numeric_digits(num_str)
|
|
62
|
-
float = num_str.to_s("F")
|
|
63
|
-
return float.split(".").last if float.length == @digit_count + 1
|
|
64
|
-
return unless @digit_count + 1 > float.length
|
|
65
|
-
|
|
66
|
-
float.split(".")[0] + ("0" * (@digit_count - float.length + 1))
|
|
67
|
-
end
|
|
68
|
-
|
|
69
84
|
def update_digit_count(number)
|
|
70
85
|
return @digit_count unless zeros_count_in(number) == @precision
|
|
71
86
|
|
|
@@ -73,10 +88,106 @@ module Plurimath
|
|
|
73
88
|
end
|
|
74
89
|
|
|
75
90
|
def zeros_count_in(number)
|
|
76
|
-
return unless number.
|
|
91
|
+
return unless number.chars.all?(DEFAULT_STRINGS[:zero])
|
|
77
92
|
|
|
78
93
|
number.length
|
|
79
94
|
end
|
|
95
|
+
|
|
96
|
+
def round_base_string(fraction)
|
|
97
|
+
# Extract the digits we need, plus one extra digit for rounding decision
|
|
98
|
+
digits = fraction[0..frac_digit_count].chars
|
|
99
|
+
discard_char = digits.pop
|
|
100
|
+
return DEFAULT_STRINGS[:empty] unless discard_char
|
|
101
|
+
# If the discarded digit is below the rounding threshold (< base/2), truncate
|
|
102
|
+
return digits.join if DIGIT_VALUE[discard_char] < threshold
|
|
103
|
+
|
|
104
|
+
# Perform carry propagation for rounding up
|
|
105
|
+
carry = 1
|
|
106
|
+
rounded_reversed = []
|
|
107
|
+
digits.reverse_each do |digit|
|
|
108
|
+
next rounded_reversed << digit unless carry.positive?
|
|
109
|
+
|
|
110
|
+
# If current digit is at max value for this base (e.g., 'f' for base 16),
|
|
111
|
+
# set it to '0' and continue carrying
|
|
112
|
+
rounded_reversed << if DIGIT_VALUE[digit] == base.pred
|
|
113
|
+
DEFAULT_STRINGS[:zero]
|
|
114
|
+
else
|
|
115
|
+
# Otherwise, increment this digit and stop carrying
|
|
116
|
+
carry = 0
|
|
117
|
+
next_mapping_char(digit)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# If we still have a carry after processing all fractional digits,
|
|
122
|
+
# we need to round up the integer part
|
|
123
|
+
round_integer(rounded_reversed, carry) if carry.positive?
|
|
124
|
+
rounded_reversed.reverse.join unless rounded_reversed.empty?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def round_integer(fraction_digits_reversed, carry = 1)
|
|
128
|
+
# Propagate carry into the integer part, updating the formatted result
|
|
129
|
+
incremented, carry = increment_integer_digits(raw_integer.chars.reverse, carry)
|
|
130
|
+
new_integer = [incremented]
|
|
131
|
+
if carry.positive?
|
|
132
|
+
# If carry propagates through all integer digits (e.g., 9+1=10 in base 10),
|
|
133
|
+
# we need to prepend '1' and remove one fractional digit to maintain digit_count
|
|
134
|
+
fraction_digits_reversed.pop
|
|
135
|
+
new_integer.insert(0, "1")
|
|
136
|
+
end
|
|
137
|
+
# Update the result array with the new integer value
|
|
138
|
+
@result[0] = @integer_formatter.format_groups(new_integer.join)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def increment_integer_digits(digits, carry)
|
|
142
|
+
# Skip over separator characters while propagating carry through integer digits
|
|
143
|
+
str_chars = [decimal, @int_group]
|
|
144
|
+
digits.each_with_index do |digit, index|
|
|
145
|
+
next if str_chars.include?(digit)
|
|
146
|
+
|
|
147
|
+
if DIGIT_VALUE[digit] == base.pred
|
|
148
|
+
# Digit is at max value (e.g., 'f' for base 16), set to '0' and continue carry
|
|
149
|
+
digits[index] = DEFAULT_STRINGS[:zero]
|
|
150
|
+
else
|
|
151
|
+
# Increment this digit and stop carrying
|
|
152
|
+
digits[index] = next_mapping_char(digit)
|
|
153
|
+
break carry = 0
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
[digits.reverse.join, carry]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def change_base(number)
|
|
161
|
+
# Convert fractional part from base 10 to target base using rational arithmetic
|
|
162
|
+
# to avoid floating-point rounding errors.
|
|
163
|
+
# Algorithm: repeatedly multiply fraction by base, extract integer part as next digit
|
|
164
|
+
# Represent the fractional part exactly as a rational number to avoid
|
|
165
|
+
# binary floating-point rounding errors when converting bases.
|
|
166
|
+
# Note: The input `number` is always in decimal (base 10) format,
|
|
167
|
+
# so we use 10 as the denominator base regardless of the target base.
|
|
168
|
+
fraction = Rational(number.to_i, 10**number.length)
|
|
169
|
+
|
|
170
|
+
base_result = []
|
|
171
|
+
digits = @precision || number.length
|
|
172
|
+
|
|
173
|
+
digits.times do
|
|
174
|
+
fraction *= base
|
|
175
|
+
digit = fraction.to_i
|
|
176
|
+
alpha_digit = HEX_ALPHANUMERIC[digit]
|
|
177
|
+
base_result << alpha_digit
|
|
178
|
+
fraction -= digit # Remove integer part, keep only fractional part
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
base_result.join
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def raw_integer
|
|
185
|
+
@result[0].delete(@int_group.to_s)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def frac_digit_count
|
|
189
|
+
@digit_count - raw_integer.length
|
|
190
|
+
end
|
|
80
191
|
end
|
|
81
192
|
end
|
|
82
193
|
end
|
|
@@ -3,28 +3,26 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
module Formatter
|
|
5
5
|
module Numbers
|
|
6
|
-
class Integer
|
|
7
|
-
attr_reader :
|
|
6
|
+
class Integer < Base
|
|
7
|
+
attr_reader :separator, :groups
|
|
8
|
+
|
|
9
|
+
DEFAULT_SEPARATOR = ","
|
|
8
10
|
|
|
9
11
|
def initialize(symbols = {})
|
|
10
|
-
|
|
11
|
-
@
|
|
12
|
+
super
|
|
13
|
+
@groups = symbols[:group_digits] || 3
|
|
14
|
+
@separator = symbols[:group] || DEFAULT_SEPARATOR
|
|
12
15
|
end
|
|
13
16
|
|
|
14
|
-
def apply(number
|
|
15
|
-
format_groups(number)
|
|
17
|
+
def apply(number)
|
|
18
|
+
format_groups(number_to_base(number))
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def format_groups(string)
|
|
19
|
-
return string if groups.empty?
|
|
20
|
-
|
|
21
22
|
tokens = []
|
|
22
23
|
|
|
23
|
-
tokens << chop_group(string, groups.first)
|
|
24
|
-
string = string[0...-tokens.first.size]
|
|
25
|
-
|
|
26
24
|
until string.empty?
|
|
27
|
-
tokens << chop_group(string, groups
|
|
25
|
+
tokens << chop_group(string, groups)
|
|
28
26
|
string = string[0...-tokens.last.size]
|
|
29
27
|
end
|
|
30
28
|
|
|
@@ -34,6 +32,12 @@ module Plurimath
|
|
|
34
32
|
def chop_group(string, size)
|
|
35
33
|
string.slice([string.size - size, 0].max, size)
|
|
36
34
|
end
|
|
35
|
+
|
|
36
|
+
def number_to_base(number)
|
|
37
|
+
return number if base_default?
|
|
38
|
+
|
|
39
|
+
number.to_i.to_s(base)
|
|
40
|
+
end
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
end
|
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
module Plurimath
|
|
4
4
|
module Formatter
|
|
5
5
|
module Numbers
|
|
6
|
-
class Significant
|
|
7
|
-
attr_accessor :
|
|
6
|
+
class Significant < Base
|
|
7
|
+
attr_accessor :decimal, :significant
|
|
8
8
|
|
|
9
9
|
def initialize(symbols)
|
|
10
|
-
|
|
10
|
+
super
|
|
11
11
|
@decimal = symbols[:decimal]
|
|
12
12
|
@significant = symbols[:significant].to_i
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def apply(string, int_format, frac_format)
|
|
16
16
|
return string if significant.zero?
|
|
17
|
-
|
|
18
|
-
chars = string.
|
|
19
|
-
|
|
20
|
-
return string if count_chars(chars, true) == significant
|
|
17
|
+
# Check if string contains any non-zero digit (works across all bases 2-16)
|
|
18
|
+
chars = string.chars
|
|
19
|
+
return string if skip_significant_processing?(chars)
|
|
21
20
|
|
|
22
21
|
string = signify(chars)
|
|
23
22
|
integer, fraction = string.split(decimal)
|
|
@@ -29,77 +28,73 @@ module Plurimath
|
|
|
29
28
|
protected
|
|
30
29
|
|
|
31
30
|
def signify(chars)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
new_chars = []
|
|
35
|
-
sig_count = significant
|
|
36
|
-
chars.each_with_index do |char, ind|
|
|
37
|
-
frac_part ||= char == decimal
|
|
38
|
-
sig_num ||= char.match?(/[1-9]/)
|
|
39
|
-
break if sig_count.zero?
|
|
40
|
-
|
|
41
|
-
new_chars << char
|
|
42
|
-
next unless sig_num
|
|
43
|
-
next unless char.match?(/[0-9]/)
|
|
44
|
-
|
|
45
|
-
sig_count -= 1
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
if sig_count > 0
|
|
31
|
+
new_chars, frac_part, sig_count = process_chars(chars)
|
|
32
|
+
if sig_count.positive?
|
|
49
33
|
new_chars << decimal unless frac_part
|
|
50
34
|
else
|
|
51
35
|
remain_chars = count_chars(chars, frac_part) - significant
|
|
52
|
-
if remain_chars
|
|
36
|
+
if remain_chars.positive?
|
|
53
37
|
round_str(chars, new_chars, frac_part)
|
|
54
|
-
|
|
38
|
+
# After rounding, recalculate remain_chars only for fractional numbers
|
|
39
|
+
# where we need to adjust padding based on actual significant digits
|
|
40
|
+
if frac_part
|
|
41
|
+
# Check if decimal point still exists after rounding
|
|
42
|
+
has_decimal = new_chars.include?(decimal)
|
|
43
|
+
if has_decimal
|
|
44
|
+
# Fractional part still exists, recalculate padding
|
|
45
|
+
actual_sig = count_significant_digits(new_chars)
|
|
46
|
+
remain_chars = [significant - actual_sig, 0].max
|
|
47
|
+
else
|
|
48
|
+
# Rounding eliminated the fractional part from a number that originally had one
|
|
49
|
+
# Don't add trailing zeros in this case
|
|
50
|
+
remain_chars = 0
|
|
51
|
+
frac_part = false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
# For integer-only numbers (frac_part = false from the start),
|
|
55
|
+
# remain_chars stays as calculated initially
|
|
55
56
|
end
|
|
56
|
-
new_chars << ("0" * remain_chars) unless frac_part && sig_char_count(new_chars)
|
|
57
|
+
new_chars << ("0" * remain_chars) unless frac_part && sig_char_count?(new_chars)
|
|
57
58
|
end
|
|
58
59
|
new_chars.join
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def round_str(chars, array, frac_part)
|
|
62
63
|
arr_len = array.length
|
|
63
|
-
char_ind = chars[arr_len]
|
|
64
|
-
return unless chars[char_ind]
|
|
64
|
+
char_ind = DIGIT_VALUE.key?(chars[arr_len]) ? arr_len : arr_len.next
|
|
65
|
+
return unless char_ind < chars.length && DIGIT_VALUE[chars[char_ind]] >= threshold
|
|
65
66
|
|
|
66
67
|
frac_part = false if chars[arr_len] == decimal
|
|
67
|
-
|
|
68
|
+
carry = false
|
|
68
69
|
array.reverse!.each_with_index do |char, ind|
|
|
69
70
|
if char == decimal
|
|
70
71
|
array[ind] = ""
|
|
71
72
|
frac_part = false
|
|
72
73
|
next
|
|
73
74
|
end
|
|
74
|
-
next unless
|
|
75
|
+
next unless DIGIT_VALUE.key?(char)
|
|
75
76
|
|
|
76
|
-
if char ==
|
|
77
|
-
|
|
77
|
+
if DIGIT_VALUE[char] == base.pred
|
|
78
|
+
carry = true
|
|
78
79
|
array[ind] = frac_part ? "" : "0"
|
|
79
|
-
|
|
80
|
+
else
|
|
81
|
+
array[ind] = next_mapping_char(char)
|
|
82
|
+
carry = false
|
|
83
|
+
break
|
|
80
84
|
end
|
|
81
|
-
char.next!
|
|
82
|
-
prev_ten = false
|
|
83
|
-
break
|
|
84
85
|
end
|
|
85
|
-
array << "1" if
|
|
86
|
+
array << "1" if carry
|
|
86
87
|
array.reverse!
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
def count_chars(chars, fraction)
|
|
90
|
-
|
|
91
|
+
char_count = 0
|
|
91
92
|
chars.each do |char|
|
|
92
93
|
break if char == decimal && !fraction
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
char_count += 1 if DIGIT_VALUE.key?(char)
|
|
95
96
|
end
|
|
96
|
-
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def change_format(string, separator, group)
|
|
100
|
-
tokens = []
|
|
101
|
-
tokens << string&.slice!(0, group) until string&.empty?
|
|
102
|
-
tokens.compact.join(separator)
|
|
97
|
+
char_count
|
|
103
98
|
end
|
|
104
99
|
|
|
105
100
|
def format_groups(format, string)
|
|
@@ -110,16 +105,53 @@ module Plurimath
|
|
|
110
105
|
string.split(format.separator).join
|
|
111
106
|
end
|
|
112
107
|
|
|
113
|
-
def sig_char_count(chars)
|
|
108
|
+
def sig_char_count?(chars)
|
|
114
109
|
start_counting = false
|
|
115
|
-
|
|
110
|
+
char_count = 0
|
|
116
111
|
chars.each do |char|
|
|
117
|
-
start_counting = true if char
|
|
112
|
+
start_counting = true if DIGIT_VALUE[char]&.positive?
|
|
118
113
|
next unless start_counting
|
|
119
114
|
|
|
120
|
-
|
|
115
|
+
char_count += 1 if DIGIT_VALUE.key?(char)
|
|
116
|
+
end
|
|
117
|
+
char_count == significant
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def process_chars(chars, sig_num: false, frac_part: false)
|
|
121
|
+
sig_count = significant
|
|
122
|
+
new_chars = []
|
|
123
|
+
chars.each do |char|
|
|
124
|
+
frac_part ||= char == decimal
|
|
125
|
+
sig_num ||= DIGIT_VALUE[char]&.positive?
|
|
126
|
+
break if sig_count.zero?
|
|
127
|
+
|
|
128
|
+
new_chars << char
|
|
129
|
+
next unless sig_num
|
|
130
|
+
next unless DIGIT_VALUE.key?(char)
|
|
131
|
+
|
|
132
|
+
sig_count -= 1
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
[new_chars, frac_part, sig_count]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def skip_significant_processing?(chars)
|
|
139
|
+
# Skip if no significant digits exist, or if we already have the exact count needed
|
|
140
|
+
chars.none? { |c| DIGIT_VALUE.key?(c) && DIGIT_VALUE[c].positive? } ||
|
|
141
|
+
count_chars(chars, true) == significant
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def count_significant_digits(chars)
|
|
145
|
+
# Count actual significant digits in the character array
|
|
146
|
+
# Leading zeros don't count as significant
|
|
147
|
+
start_counting = false
|
|
148
|
+
char_count = 0
|
|
149
|
+
chars.each do |char|
|
|
150
|
+
start_counting = true if DIGIT_VALUE[char]&.positive?
|
|
151
|
+
next unless start_counting
|
|
152
|
+
char_count += 1 if DIGIT_VALUE.key?(char)
|
|
121
153
|
end
|
|
122
|
-
|
|
154
|
+
char_count
|
|
123
155
|
end
|
|
124
156
|
end
|
|
125
157
|
end
|
data/lib/plurimath/formatter.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "errors/formatter/unsupported_base"
|
|
3
4
|
require_relative "formatter/numeric_formatter"
|
|
4
5
|
require_relative "formatter/supported_locales"
|
|
6
|
+
require_relative "formatter/numbers/base"
|
|
5
7
|
require_relative "formatter/numbers/integer"
|
|
6
8
|
require_relative "formatter/numbers/fraction"
|
|
7
9
|
require_relative "formatter/numbers/significant"
|
data/lib/plurimath/math/core.rb
CHANGED
|
@@ -419,11 +419,18 @@ module Plurimath
|
|
|
419
419
|
def mstyle_value=(value)
|
|
420
420
|
return if value.nil? || value.empty?
|
|
421
421
|
|
|
422
|
+
replacing_order = value.length > 1 && value.any?(String)
|
|
422
423
|
update(
|
|
423
424
|
filter_values(
|
|
424
425
|
replace_order_with_value(
|
|
425
426
|
@value,
|
|
426
|
-
Array(
|
|
427
|
+
Array(
|
|
428
|
+
filter_values(
|
|
429
|
+
value,
|
|
430
|
+
array_to_instance: true,
|
|
431
|
+
replacing_order: replacing_order,
|
|
432
|
+
),
|
|
433
|
+
),
|
|
427
434
|
"mstyle"
|
|
428
435
|
)
|
|
429
436
|
)
|
|
@@ -624,7 +631,7 @@ module Plurimath
|
|
|
624
631
|
end
|
|
625
632
|
|
|
626
633
|
def parse_error!(type)
|
|
627
|
-
|
|
634
|
+
raise ParseError.new(input_string, type)
|
|
628
635
|
end
|
|
629
636
|
|
|
630
637
|
def omml_br_tag
|
|
@@ -986,9 +993,7 @@ module Plurimath
|
|
|
986
993
|
# Dd derivative nodes end
|
|
987
994
|
|
|
988
995
|
def type_error!(type)
|
|
989
|
-
raise Math::InvalidTypeError.new(
|
|
990
|
-
"Invalid type provided: #{type}. Must be one of #{MATH_ZONE_TYPES.join(', ')}.",
|
|
991
|
-
)
|
|
996
|
+
raise Math::InvalidTypeError.new(type)
|
|
992
997
|
end
|
|
993
998
|
end
|
|
994
999
|
end
|
|
@@ -51,15 +51,78 @@ module Plurimath
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def font_styles(display_style, sty: "p", scr: nil, options:)
|
|
54
|
+
children = flatten_omml_nodes(
|
|
55
|
+
Array(parameter_one&.font_style_t_tag(display_style, options: options)),
|
|
56
|
+
)
|
|
57
|
+
return [empty_font_style_r_tag(sty, scr)] if children.empty?
|
|
58
|
+
|
|
59
|
+
if children.all? { |node| node.is_a?(String) || t_tag_node?(node) }
|
|
60
|
+
r_tag = empty_font_style_r_tag(sty, scr)
|
|
61
|
+
Utility.update_nodes(r_tag, children)
|
|
62
|
+
return [r_tag]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
children.flat_map do |node|
|
|
66
|
+
if r_tag_node?(node)
|
|
67
|
+
inject_font_style_rpr(node, sty, scr)
|
|
68
|
+
[node]
|
|
69
|
+
elsif t_tag_node?(node)
|
|
70
|
+
r_tag = empty_font_style_r_tag(sty, scr)
|
|
71
|
+
r_tag << node
|
|
72
|
+
[r_tag]
|
|
73
|
+
else
|
|
74
|
+
inject_font_style_recursive(node, sty, scr)
|
|
75
|
+
[node]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def flatten_omml_nodes(nodes)
|
|
81
|
+
nodes.each_with_object([]) do |node, result|
|
|
82
|
+
if node.is_a?(Array)
|
|
83
|
+
result.concat(flatten_omml_nodes(node))
|
|
84
|
+
elsif !node.nil?
|
|
85
|
+
result << node
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def t_tag_node?(node)
|
|
91
|
+
node.respond_to?(:name) && node.name == "m:t"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def r_tag_node?(node)
|
|
95
|
+
node.respond_to?(:name) && node.name == "m:r"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def empty_font_style_r_tag(sty, scr)
|
|
54
99
|
r_tag = Utility.ox_element("r", namespace: "m")
|
|
55
100
|
rpr_tag = Utility.ox_element("rPr", namespace: "m")
|
|
56
101
|
rpr_tag << Utility.ox_element("scr", namespace: "m", attributes: { "m:val": scr }) if scr
|
|
57
102
|
rpr_tag << Utility.ox_element("sty", namespace: "m", attributes: { "m:val": sty }) if sty
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
r_tag << rpr_tag
|
|
104
|
+
r_tag
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def inject_font_style_rpr(r_node, sty, scr)
|
|
108
|
+
return if r_node.nodes.any? { |n| n.respond_to?(:name) && n.name == "m:rPr" }
|
|
109
|
+
|
|
110
|
+
rpr_tag = Utility.ox_element("rPr", namespace: "m")
|
|
111
|
+
rpr_tag << Utility.ox_element("scr", namespace: "m", attributes: { "m:val": scr }) if scr
|
|
112
|
+
rpr_tag << Utility.ox_element("sty", namespace: "m", attributes: { "m:val": sty }) if sty
|
|
113
|
+
r_node.insert_in_nodes(0, rpr_tag)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def inject_font_style_recursive(node, sty, scr)
|
|
117
|
+
return unless node.respond_to?(:nodes)
|
|
118
|
+
|
|
119
|
+
node.nodes.each do |child|
|
|
120
|
+
if r_tag_node?(child)
|
|
121
|
+
inject_font_style_rpr(child, sty, scr)
|
|
122
|
+
elsif child.respond_to?(:nodes)
|
|
123
|
+
inject_font_style_recursive(child, sty, scr)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
63
126
|
end
|
|
64
127
|
|
|
65
128
|
def to_asciimath_math_zone(spacing, last = false, _, options:)
|
data/lib/plurimath/math.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "errors"
|
|
3
4
|
require_relative "asciimath"
|
|
4
5
|
require_relative "omml"
|
|
5
6
|
require_relative "unicode_math"
|
|
@@ -27,9 +28,6 @@ require "yaml"
|
|
|
27
28
|
|
|
28
29
|
module Plurimath
|
|
29
30
|
module Math
|
|
30
|
-
class ParseError < StandardError; end
|
|
31
|
-
class InvalidTypeError < TypeError; end
|
|
32
|
-
|
|
33
31
|
VALID_TYPES = {
|
|
34
32
|
omml: Omml,
|
|
35
33
|
html: Html,
|
|
@@ -41,15 +39,18 @@ module Plurimath
|
|
|
41
39
|
}.freeze
|
|
42
40
|
|
|
43
41
|
def parse(text, type)
|
|
44
|
-
|
|
42
|
+
raise InvalidTypeError.new unless valid_type?(type)
|
|
45
43
|
|
|
46
44
|
begin
|
|
47
45
|
klass = klass_from_type(type)
|
|
48
46
|
formula = klass.new(text).to_formula
|
|
49
47
|
formula.input_string = text
|
|
50
48
|
formula
|
|
49
|
+
rescue ParseError
|
|
50
|
+
# Re-raise ParseError from lower layers unchanged to preserve specialized error types
|
|
51
|
+
raise
|
|
51
52
|
rescue => ee
|
|
52
|
-
|
|
53
|
+
raise ParseError.new(text, type), cause: nil
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -59,30 +60,11 @@ module Plurimath
|
|
|
59
60
|
VALID_TYPES[type_string_or_sym.to_sym]
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
def parse_error!(text, type)
|
|
63
|
-
message = <<~MESSAGE
|
|
64
|
-
[plurimath] Error: Failed to parse the following formula with type `#{type}`.
|
|
65
|
-
[plurimath] Please first manually validate the formula.
|
|
66
|
-
[plurimath] If this is a bug, please report the formula at our issue tracker at:
|
|
67
|
-
[plurimath] https://github.com/plurimath/plurimath/issues
|
|
68
|
-
---- FORMULA BEGIN ----
|
|
69
|
-
#{text}
|
|
70
|
-
---- FORMULA END ----
|
|
71
|
-
MESSAGE
|
|
72
|
-
raise ParseError.new(message), cause: nil
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def type_error!
|
|
76
|
-
raise InvalidTypeError.new(
|
|
77
|
-
"`type` must be one of: `#{VALID_TYPES.keys.join('`, `')}`",
|
|
78
|
-
)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
63
|
def valid_type?(type)
|
|
82
64
|
(type.is_a?(::Symbol) || type.is_a?(String)) &&
|
|
83
65
|
VALID_TYPES.key?(type.to_sym)
|
|
84
66
|
end
|
|
85
67
|
|
|
86
|
-
module_function :parse, :klass_from_type, :
|
|
68
|
+
module_function :parse, :klass_from_type, :valid_type?
|
|
87
69
|
end
|
|
88
70
|
end
|
data/lib/plurimath/mathml.rb
CHANGED
data/lib/plurimath/unitsml.rb
CHANGED
|
@@ -5,24 +5,17 @@ module Plurimath
|
|
|
5
5
|
class Unitsml
|
|
6
6
|
attr_accessor :text
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# Match ^ followed by content containing letters (indicating variables as exponents, which are invalid)
|
|
9
|
+
# Simplified regex to avoid catastrophic backtracking (ReDoS vulnerability)
|
|
10
|
+
VALID_UNITSML = /\^\(?-?\d*[a-z]/i
|
|
9
11
|
|
|
10
12
|
def initialize(text)
|
|
11
13
|
@text = text
|
|
12
|
-
raise Math::ParseError.new(
|
|
14
|
+
raise Math::ParseError.new(text, :invalid_unitsml) if text.match?(VALID_UNITSML)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def to_formula
|
|
16
18
|
Math::Function::Unitsml.new(text)
|
|
17
19
|
end
|
|
18
|
-
|
|
19
|
-
def error_message
|
|
20
|
-
<<~MESSAGE
|
|
21
|
-
[plurimath] Invalid formula `#{@text}`.
|
|
22
|
-
[plurimath] The use of a variable as an exponent is not valid.
|
|
23
|
-
[plurimath] If this is a bug, please report the formula at our issue tracker at:
|
|
24
|
-
[plurimath] https://github.com/plurimath/plurimath/issues
|
|
25
|
-
MESSAGE
|
|
26
|
-
end
|
|
27
20
|
end
|
|
28
21
|
end
|
data/lib/plurimath/version.rb
CHANGED
data/plurimath.gemspec
CHANGED
|
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.add_dependency 'mml'
|
|
29
29
|
spec.add_dependency 'thor'
|
|
30
30
|
spec.add_dependency 'parslet'
|
|
31
|
+
spec.add_dependency 'ostruct'
|
|
31
32
|
spec.add_dependency 'unitsml'
|
|
32
33
|
spec.add_dependency 'bigdecimal'
|
|
33
34
|
spec.add_dependency 'lutaml-model'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: plurimath
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-04-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ox
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: ostruct
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
84
|
name: unitsml
|
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -149,8 +163,13 @@ files:
|
|
|
149
163
|
- lib/plurimath/asciimath/parser.rb
|
|
150
164
|
- lib/plurimath/asciimath/transform.rb
|
|
151
165
|
- lib/plurimath/cli.rb
|
|
166
|
+
- lib/plurimath/errors.rb
|
|
167
|
+
- lib/plurimath/errors/formatter/unsupported_base.rb
|
|
168
|
+
- lib/plurimath/errors/invalid_type_error.rb
|
|
169
|
+
- lib/plurimath/errors/parse_error.rb
|
|
152
170
|
- lib/plurimath/formatter.rb
|
|
153
171
|
- lib/plurimath/formatter/number_formatter.rb
|
|
172
|
+
- lib/plurimath/formatter/numbers/base.rb
|
|
154
173
|
- lib/plurimath/formatter/numbers/fraction.rb
|
|
155
174
|
- lib/plurimath/formatter/numbers/integer.rb
|
|
156
175
|
- lib/plurimath/formatter/numbers/significant.rb
|