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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ecaec3071c4b2925d95eceffec830c054c840c7813db4f0b8f4e6e95c366ced
4
- data.tar.gz: 19adb5616581e744f45805329324b950dcad692c670fa07153579ff71f319d05
3
+ metadata.gz: 3e78c38692b0343fbe7666f309339f0e94180803b8aa81e48c71897d37eab606
4
+ data.tar.gz: 44c7c9ae6c2fb5d9b09d869c09c91775172f77b9bf27dee0aeb511f5bb272046
5
5
  SHA512:
6
- metadata.gz: a4d1ee78250433329ab39e542804c8199afcf9afecb43027c76611c9619fc6f4eb72d2e49ff0b93c801d53f2986ee97b926217c0fe99c57898992aa900f12079
7
- data.tar.gz: 020112a942828e93440ebdc3c95af0a720c8766411f4edf19cca7ec1f39423c67b8808a6cb95ef01e99429dc6f7d06d322f63829b3b2f7a832d2e308fd1aedd9
6
+ metadata.gz: 4d79d19001092a56d19ae94705c3777460fd24cd57ebca84b1a6ee1d9b68ed473e0cf3799d45c58ba6af6f3aa0e56dc8b8160559bc43c70404885adb024a2232
7
+ data.tar.gz: c256d08993caa5a78caded66b7aaa65c8c7a52f413bb14dacfafbe29f2984fa604516942a8b28b5684486f34eb99ca3da8f03dc5f11513a41381484e0a507d7d
@@ -15,6 +15,9 @@ on:
15
15
 
16
16
  jobs:
17
17
  release:
18
+ permissions:
19
+ contents: write
20
+ id-token: write
18
21
  uses: metanorma/ci/.github/workflows/rubygems-release.yml@main
19
22
  with:
20
23
  next_version: ${{ github.event.inputs.next_version }}
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 -e <file_path> -t unicodemath
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 `E`). (used in the
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. Value should be a Numeric,
747
- i.e. Integer, Float, or BigDecimal. If not provided, an `ArgumentError` will be
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
- `format_string`:: (default: `nil`, disabled) a string value (localize_number)
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
- # format_string: <string value> if provided
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:, format_string:) <2>
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:, format_string:)
1298
+ def initialize(locale:, precision:, options:, string_format:)
1179
1299
  super
1180
1300
  end
1181
1301
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "math"
4
3
  module Plurimath
5
4
  class Asciimath
6
5
  attr_accessor :text
data/lib/plurimath/cli.rb CHANGED
@@ -77,7 +77,7 @@ module Plurimath
77
77
  no_commands do
78
78
  def warn_and_exit(message)
79
79
  warn(message)
80
- exit 1
80
+ abort
81
81
  end
82
82
  end
83
83
  end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors/parse_error"
4
+ require_relative "errors/invalid_type_error"
5
+ require_relative "errors/formatter/unsupported_base"
@@ -3,17 +3,33 @@
3
3
  module Plurimath
4
4
  module Formatter
5
5
  class NumberFormatter
6
- attr_reader :number, :data_reader, :prefix
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: ".".freeze,
10
- f: "F".freeze,
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
- @prefix = "-" if number.negative?
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, data_reader)
26
- result << fraction_format.apply(frac, data_reader, int) if 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 = "+#{result}" if number.positive? && data_reader[:number_sign].to_s == "plus"
30
- "#{prefix}#{result}"
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
- round_to(number, precision).abs.fix.to_s(STRING_SYMBOLS[:f])
95
+ abs.fix.to_s(STRING_SYMBOLS[:f])
58
96
  else
59
- round_to(number, precision).abs.round(precision).to_s(STRING_SYMBOLS[:f])
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
- @precision = 3
11
- @decimal = symbols[:decimal] || '.'
12
- @separator = symbols[:fraction_group].to_s
13
- @group = symbols[:fraction_group_digits]
14
- @digit_count = symbols[:digit_count] || nil
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, options = {}, int = "")
18
- precision = options[:precision] || @precision
19
- return "" unless precision > 0
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(int, fraction)
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 = change_format(number) if 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 + "0" * (precision - number.length)
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
- change_format(string)
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
- tokens << string&.slice!(0, (group || string.length)) until string&.empty?
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(int, fraction)
48
- integer = int + "." + fraction
49
- int_length = integer.length - 1 # integer length; excluding the decimal point
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
- number_string = BigDecimal(integer).round(@digit_count - int.length)
53
- numeric_digits(number_string) if @digit_count > int.length
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 + ("0" * (update_digit_count(fraction) - int_length))
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.split('').all? { |digit| digit == "0" }
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 :format, :separator, :groups
6
+ class Integer < Base
7
+ attr_reader :separator, :groups
8
+
9
+ DEFAULT_SEPARATOR = ","
8
10
 
9
11
  def initialize(symbols = {})
10
- @groups = Array(symbols[:group_digits] || 3)
11
- @separator = symbols[:group] || ','
12
+ super
13
+ @groups = symbols[:group_digits] || 3
14
+ @separator = symbols[:group] || DEFAULT_SEPARATOR
12
15
  end
13
16
 
14
- def apply(number, options = {})
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.last)
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 :symbols, :decimal, :significant
6
+ class Significant < Base
7
+ attr_accessor :decimal, :significant
8
8
 
9
9
  def initialize(symbols)
10
- @symbols = symbols
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
- return string unless string.match?(/[1-9]/)
18
- chars = string.split("")
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
- sig_num = false
33
- frac_part = false
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 > 0
36
+ if remain_chars.positive?
53
37
  round_str(chars, new_chars, frac_part)
54
- remain_chars = 0 if frac_part && remain_chars == 1
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]&.match?(/[0-9]/) ? arr_len : arr_len + 1
64
- return unless chars[char_ind]&.match?(/[5-9]/)
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
- prev_ten = false
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 char.match?(/[0-9]/)
75
+ next unless DIGIT_VALUE.key?(char)
75
76
 
76
- if char == "9"
77
- prev_ten = true
77
+ if DIGIT_VALUE[char] == base.pred
78
+ carry = true
78
79
  array[ind] = frac_part ? "" : "0"
79
- next
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 prev_ten
86
+ array << "1" if carry
86
87
  array.reverse!
87
88
  end
88
89
 
89
90
  def count_chars(chars, fraction)
90
- counting = 0
91
+ char_count = 0
91
92
  chars.each do |char|
92
93
  break if char == decimal && !fraction
93
94
 
94
- counting += 1 if char.match?(/\d/)
95
+ char_count += 1 if DIGIT_VALUE.key?(char)
95
96
  end
96
- counting
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
- counting = 0
110
+ char_count = 0
116
111
  chars.each do |char|
117
- start_counting = true if char.match(/[1-9]/)
112
+ start_counting = true if DIGIT_VALUE[char]&.positive?
118
113
  next unless start_counting
119
114
 
120
- counting += 1 if char.match?(/\d/)
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
- counting == significant
154
+ char_count
123
155
  end
124
156
  end
125
157
  end
@@ -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"
@@ -352,6 +352,7 @@ module Plurimath
352
352
  :@using_default,
353
353
  :@displaystyle,
354
354
  :@__encoding,
355
+ :@__register,
355
356
  :@__ordered,
356
357
  :@unitsml,
357
358
  :@__mixed,
@@ -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(filter_values(value, array_to_instance: true)),
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
- Math.parse_error!(input_string, type)
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
- Utility.update_nodes(
59
- (r_tag << rpr_tag),
60
- Array(parameter_one&.font_style_t_tag(display_style, options: options)),
61
- )
62
- [r_tag]
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:)
@@ -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
- type_error! unless valid_type?(type)
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
- parse_error!(text, type.to_sym)
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, :parse_error!, :type_error!, :valid_type?
68
+ module_function :parse, :klass_from_type, :valid_type?
87
69
  end
88
70
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "math"
4
-
5
3
  module Plurimath
6
4
  class Mathml
7
5
  attr_accessor :text
@@ -5,24 +5,17 @@ module Plurimath
5
5
  class Unitsml
6
6
  attr_accessor :text
7
7
 
8
- VALID_UNITSML = %r{\^(([^\s][^*\/,"]*?[a-z]+)|(\([^-\d]+\)|[^\(\d-]+))}
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(error_message) if text.match?(VALID_UNITSML)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plurimath
4
- VERSION = "0.9.11"
4
+ VERSION = "0.10.0"
5
5
  end
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.9.11
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: 2025-11-05 00:00:00.000000000 Z
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