plurimath 0.10.7 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +3 -2
  3. data/README.adoc +343 -44
  4. data/lib/plurimath/asciimath/parse.rb +6 -2
  5. data/lib/plurimath/configuration.rb +17 -0
  6. data/lib/plurimath/deprecation.rb +81 -0
  7. data/lib/plurimath/errors/configuration_error.rb +27 -0
  8. data/lib/plurimath/errors/deprecation_error.rb +33 -0
  9. data/lib/plurimath/errors/error.rb +6 -0
  10. data/lib/plurimath/errors/formatter/unsupported_base.rb +1 -1
  11. data/lib/plurimath/errors/formatter/unsupported_locale.rb +18 -0
  12. data/lib/plurimath/errors/omml/unsupported_node_error.rb +1 -1
  13. data/lib/plurimath/errors/parse_error.rb +1 -1
  14. data/lib/plurimath/errors/parse_option_error.rb +34 -0
  15. data/lib/plurimath/formatter/numbers/base.rb +18 -8
  16. data/lib/plurimath/formatter/numbers/base_notation.rb +67 -0
  17. data/lib/plurimath/formatter/numbers/digit_sequence.rb +96 -0
  18. data/lib/plurimath/formatter/numbers/format_options.rb +141 -0
  19. data/lib/plurimath/formatter/numbers/fraction.rb +50 -93
  20. data/lib/plurimath/formatter/numbers/integer.rb +30 -6
  21. data/lib/plurimath/formatter/numbers/notation_renderer.rb +128 -0
  22. data/lib/plurimath/formatter/numbers/number_renderer.rb +66 -0
  23. data/lib/plurimath/formatter/numbers/parts.rb +69 -0
  24. data/lib/plurimath/formatter/numbers/parts_renderer.rb +30 -0
  25. data/lib/plurimath/formatter/numbers/precision_resolver.rb +54 -0
  26. data/lib/plurimath/formatter/numbers/sign_renderer.rb +28 -0
  27. data/lib/plurimath/formatter/numbers/significant.rb +77 -103
  28. data/lib/plurimath/formatter/numbers/source.rb +120 -0
  29. data/lib/plurimath/formatter/numbers/symbol_resolver.rb +55 -0
  30. data/lib/plurimath/formatter/numbers.rb +11 -0
  31. data/lib/plurimath/formatter/standard.rb +32 -42
  32. data/lib/plurimath/formatter/supported_locales.rb +27 -0
  33. data/lib/plurimath/formatter.rb +1 -2
  34. data/lib/plurimath/html/constants.rb +2 -0
  35. data/lib/plurimath/html/parse.rb +77 -14
  36. data/lib/plurimath/html/parser.rb +15 -3
  37. data/lib/plurimath/html/transform.rb +193 -91
  38. data/lib/plurimath/html/transform_utility.rb +61 -0
  39. data/lib/plurimath/html.rb +1 -0
  40. data/lib/plurimath/latex/parse.rb +7 -1
  41. data/lib/plurimath/latex/transform.rb +5 -5
  42. data/lib/plurimath/math/function/lim.rb +6 -0
  43. data/lib/plurimath/math/number.rb +8 -2
  44. data/lib/plurimath/math/symbols/cdot.rb +1 -1
  45. data/lib/plurimath/math/symbols/exclam.rb +1 -1
  46. data/lib/plurimath/math/symbols/minus.rb +1 -1
  47. data/lib/plurimath/math/symbols/percent.rb +1 -1
  48. data/lib/plurimath/math/symbols/pi.rb +1 -1
  49. data/lib/plurimath/math/symbols/slash.rb +1 -1
  50. data/lib/plurimath/math.rb +56 -8
  51. data/lib/plurimath/number_formatter.rb +57 -27
  52. data/lib/plurimath/unicode_math/parse.rb +7 -1
  53. data/lib/plurimath/unicode_math/transform.rb +2 -2
  54. data/lib/plurimath/version.rb +1 -1
  55. data/lib/plurimath.rb +23 -1
  56. metadata +21 -4
  57. data/lib/plurimath/formatter/number_formatter.rb +0 -115
  58. data/lib/plurimath/formatter/numeric_formatter.rb +0 -187
@@ -5,7 +5,7 @@ module Plurimath
5
5
  class Transform < Parslet::Transform
6
6
  rule(base: simple(:base)) { base }
7
7
  rule(over: simple(:over)) { over }
8
- rule(number: simple(:num)) { Math::Number.new(num) }
8
+ rule(number: simple(:num)) { Math::Number.new(Utility.html_entity_to_unicode(num.to_s)) }
9
9
  rule(power: simple(:power)) { power }
10
10
  rule(unary: simple(:unary)) { Utility.get_class(unary).new }
11
11
  rule(space: simple(:space)) { Math::Function::Text.new(" ") }
@@ -188,7 +188,7 @@ module Plurimath
188
188
  number: simple(:number)) do
189
189
  Math::Function::Power.new(
190
190
  power,
191
- Math::Number.new(number),
191
+ Math::Number.new(Utility.html_entity_to_unicode(number.to_s)),
192
192
  )
193
193
  end
194
194
 
@@ -428,7 +428,7 @@ module Plurimath
428
428
  rule(number: simple(:number),
429
429
  subscript: simple(:subscript)) do
430
430
  Math::Function::Base.new(
431
- Math::Number.new(number),
431
+ Math::Number.new(Utility.html_entity_to_unicode(number.to_s)),
432
432
  subscript,
433
433
  )
434
434
  end
@@ -436,7 +436,7 @@ module Plurimath
436
436
  rule(number: simple(:number),
437
437
  supscript: simple(:supscript)) do
438
438
  Math::Function::Power.new(
439
- Math::Number.new(number),
439
+ Math::Number.new(Utility.html_entity_to_unicode(number.to_s)),
440
440
  supscript,
441
441
  )
442
442
  end
@@ -445,7 +445,7 @@ module Plurimath
445
445
  subscript: simple(:subscript),
446
446
  supscript: simple(:supscript)) do
447
447
  Math::Function::PowerBase.new(
448
- Math::Number.new(number),
448
+ Math::Number.new(Utility.html_entity_to_unicode(number.to_s)),
449
449
  subscript,
450
450
  supscript,
451
451
  )
@@ -28,6 +28,12 @@ module Plurimath
28
28
  "\\#{class_name}#{first_value}#{second_value}"
29
29
  end
30
30
 
31
+ def to_html(options:)
32
+ first_value = "<i>#{parameter_one.to_html(options: options)}</i>" if parameter_one
33
+ second_value = "<i>#{parameter_two.to_html(options: options)}</i>" if parameter_two
34
+ "<i>lim</i>#{first_value}#{second_value}"
35
+ end
36
+
31
37
  def to_mathml_without_math_tag(intent, options:)
32
38
  first_value = Utility.ox_element("mo") << "lim"
33
39
  return first_value unless any_value_exist?
@@ -93,8 +93,14 @@ module Plurimath
93
93
  end
94
94
 
95
95
  def format_value_with_options(options)
96
- formatter = options[:formatter]
97
- if formatter.respond_to?(:format)
96
+ formatter = options.fetch(:formatter) do
97
+ Plurimath.configuration.number_formatter
98
+ end
99
+ return value unless formatter
100
+
101
+ if formatter.respond_to?(:format_number)
102
+ formatter.format_number(options[:formula], self)
103
+ elsif formatter.respond_to?(:format)
98
104
  formatter.format(options[:formula], self)
99
105
  elsif formatter.respond_to?(:localized_number)
100
106
  formatter.localized_number(value.to_s)
@@ -9,7 +9,7 @@ module Plurimath
9
9
  mathml: ["&#x22c5;"],
10
10
  latex: [["cdot", "&#x22c5;"], parsing_wrapper(["*"], lang: :latex)],
11
11
  omml: ["&#x22c5;"],
12
- html: ["&#x22c5;"],
12
+ html: ["&#x22c5;", "&sdot;"],
13
13
  }.freeze
14
14
 
15
15
  # output methods
@@ -10,7 +10,7 @@ module Plurimath
10
10
  mathml: ["&#x21;"],
11
11
  latex: [["exclam", "!", "&#x21;"]],
12
12
  omml: ["&#x21;"],
13
- html: ["&#x21;"],
13
+ html: ["&#x21;", "!"],
14
14
  }.freeze
15
15
 
16
16
  # output methods
@@ -10,7 +10,7 @@ module Plurimath
10
10
  mathml: ["&#x2212;", "-"],
11
11
  latex: [["minus", "-", "&#x2212;"]],
12
12
  omml: ["&#x2212;"],
13
- html: ["&#x2212;"],
13
+ html: ["&#x2212;", "-"],
14
14
  }.freeze
15
15
 
16
16
  # output methods
@@ -10,7 +10,7 @@ module Plurimath
10
10
  mathml: ["&#x25;"],
11
11
  latex: [["percent", "%", "&#x25;"]],
12
12
  omml: ["&#x25;"],
13
- html: ["&#x25;"],
13
+ html: ["&#x25;", "%"],
14
14
  }.freeze
15
15
 
16
16
  # output methods
@@ -10,7 +10,7 @@ module Plurimath
10
10
  mathml: ["&#x3c0;", "&#x1d70b;"],
11
11
  latex: ["pi", "uppi", "&#x3c0;", "&#x1d70b;"],
12
12
  omml: ["&#x3c0;", "&#x1d70b;"],
13
- html: ["&#x3C0;", "&#x1d70b;"],
13
+ html: ["&#x3C0;", "&#x3c0;", "&#x1d70b;"],
14
14
  }.freeze
15
15
 
16
16
  # output methods
@@ -10,7 +10,7 @@ module Plurimath
10
10
  mathml: ["&#x2215;"],
11
11
  latex: [["divslash", "slash", "/", "&#x2215;"]],
12
12
  omml: ["&#x2215;"],
13
- html: ["&#x2215;"],
13
+ html: ["&#x2215;", "/"],
14
14
  }.freeze
15
15
 
16
16
  # output methods
@@ -7,6 +7,7 @@ module Plurimath
7
7
  autoload :Function, "#{__dir__}/math/function"
8
8
  autoload :InvalidTypeError, "#{__dir__}/errors/invalid_type_error"
9
9
  autoload :Number, "#{__dir__}/math/number"
10
+ autoload :ParseOptionError, "#{__dir__}/errors/parse_option_error"
10
11
  autoload :ParseError, "#{__dir__}/errors/parse_error"
11
12
  autoload :Symbols, "#{__dir__}/math/symbols"
12
13
 
@@ -19,15 +20,23 @@ module Plurimath
19
20
  unicode: UnicodeMath,
20
21
  asciimath: Asciimath,
21
22
  }.freeze
23
+ SUPPORTED_PARSE_OPTIONS = %i[locale].freeze
24
+ LOCALIZED_PARSE_TYPES = %i[asciimath html latex unicode].freeze
22
25
 
23
- def parse(text, type)
26
+ module_function
27
+
28
+ def parse(text, type, **options)
24
29
  raise InvalidTypeError.new unless valid_type?(type)
25
30
 
31
+ type = type.to_sym
32
+ unknown_options = options.keys - SUPPORTED_PARSE_OPTIONS
33
+ raise_unknown_parse_options!(unknown_options) unless unknown_options.empty?
34
+
35
+ raise_unsupported_parse_option!(type, :locale) if options.key?(:locale) && !localized_parse_type?(type)
36
+ options = normalize_parse_options(options)
37
+
26
38
  begin
27
- klass = klass_from_type(type)
28
- formula = klass.new(text).to_formula
29
- formula.input_string = text
30
- formula
39
+ parse_with_configuration(text, type, options)
31
40
  rescue ParseError
32
41
  # Re-raise ParseError from lower layers unchanged to preserve specialized error types
33
42
  raise
@@ -36,17 +45,56 @@ module Plurimath
36
45
  end
37
46
  end
38
47
 
39
- private
48
+ def parse_with_configuration(text, type, options)
49
+ return parse_formula(text, type) unless options.key?(:locale)
50
+
51
+ Plurimath.with_configuration do |config|
52
+ config.locale = options.fetch(:locale)
53
+ parse_formula(text, type)
54
+ end
55
+ end
56
+
57
+ def parse_formula(text, type)
58
+ klass = klass_from_type(type)
59
+ formula = klass.new(text).to_formula
60
+ formula.input_string = text
61
+ formula
62
+ end
40
63
 
41
64
  def klass_from_type(type_string_or_sym)
42
65
  VALID_TYPES[type_string_or_sym.to_sym]
43
66
  end
44
67
 
68
+ def normalize_parse_options(options)
69
+ return options unless options.key?(:locale)
70
+
71
+ options.merge(
72
+ locale: Formatter::SupportedLocales.key_for!(options.fetch(:locale)),
73
+ )
74
+ end
75
+
76
+ def localized_parse_type?(type)
77
+ LOCALIZED_PARSE_TYPES.include?(type)
78
+ end
79
+
80
+ def raise_unknown_parse_options!(options)
81
+ raise ParseOptionError.unknown_options(
82
+ options,
83
+ supported_options: SUPPORTED_PARSE_OPTIONS,
84
+ )
85
+ end
86
+
87
+ def raise_unsupported_parse_option!(type, option)
88
+ raise ParseOptionError.unsupported_options(
89
+ type,
90
+ [option],
91
+ supported_types: LOCALIZED_PARSE_TYPES,
92
+ )
93
+ end
94
+
45
95
  def valid_type?(type)
46
96
  (type.is_a?(::Symbol) || type.is_a?(String)) &&
47
97
  VALID_TYPES.key?(type.to_sym)
48
98
  end
49
-
50
- module_function :parse, :klass_from_type, :valid_type?
51
99
  end
52
100
  end
@@ -1,52 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bigdecimal"
4
-
5
3
  module Plurimath
6
4
  class NumberFormatter
7
5
  attr_accessor :locale, :localize_number, :localizer_symbols, :precision
8
6
 
9
- def initialize(locale = "en", localize_number: nil, localizer_symbols: {},
10
- precision: nil)
7
+ def initialize(
8
+ locale = "en",
9
+ localize_number: nil,
10
+ localizer_symbols: {},
11
+ precision: nil
12
+ )
11
13
  @locale = supported_locale(locale)
12
14
  @localize_number = localize_number
13
15
  @localizer_symbols = localizer_symbols
14
16
  @precision = precision
15
17
  end
16
18
 
17
- def localized_number(number_string, locale: @locale, precision: @precision,
18
- format: {})
19
- prev_symbols = symbols(locale.to_sym).dup
20
- Formatter::NumericFormatter.new(
21
- supported_locale(locale),
22
- localize_number: localize_number,
23
- localizer_symbols: localizer_symbols,
24
- ).localized_number(
25
- number_string,
26
- locale: supported_locale(locale),
19
+ def localized_number(
20
+ number_string,
21
+ locale: @locale,
22
+ precision: @precision,
23
+ format: {}
24
+ )
25
+ locale = supported_locale(locale)
26
+ source = Formatter::Numbers::Source.new(number_string)
27
+ options = format_options(source, locale, precision, format)
28
+
29
+ if options.notation_supported?
30
+ return notation_renderer(options).render(source, options.notation)
31
+ end
32
+
33
+ render_localized_number(source, options)
34
+ end
35
+
36
+ def twitter_cldr_reader(locale: @locale)
37
+ symbols_for(supported_locale(locale), {})
38
+ end
39
+
40
+ private
41
+
42
+ def format_options(source, locale, precision, format)
43
+ Formatter::Numbers::FormatOptions.new(
44
+ source,
45
+ symbols: symbols_for(locale, format),
27
46
  precision: precision,
28
- format: format,
47
+ precision_resolver: precision_resolver,
29
48
  )
30
- ensure
31
- symbols(locale.to_sym).replace(prev_symbols)
32
49
  end
33
50
 
34
- def twitter_cldr_reader(locale: @locale)
35
- Formatter::NumericFormatter.new(
36
- supported_locale(locale),
37
- localize_number: localize_number,
51
+ def notation_renderer(options)
52
+ Formatter::Numbers::NotationRenderer.new(options)
53
+ end
54
+
55
+ def precision_resolver
56
+ @precision_resolver ||= Formatter::Numbers::PrecisionResolver.new
57
+ end
58
+
59
+ def render_localized_number(source, options)
60
+ Formatter::Numbers::NumberRenderer.new(
61
+ source,
62
+ options,
63
+ ).format
64
+ end
65
+
66
+ def symbol_resolver(locale)
67
+ Formatter::Numbers::SymbolResolver.new(
68
+ locale,
38
69
  localizer_symbols: localizer_symbols,
39
- ).twitter_cldr_reader
70
+ localize_number: localize_number,
71
+ )
40
72
  end
41
73
 
42
- private
74
+ def symbols_for(locale, format)
75
+ symbol_resolver(locale).resolve.merge(format)
76
+ end
43
77
 
44
78
  def supported_locale(locale)
45
79
  Formatter::SupportedLocales::LOCALES.key?(locale.to_sym) ? locale.to_sym : :en
46
80
  end
47
-
48
- def symbols(locale)
49
- Formatter::SupportedLocales::LOCALES[locale]
50
- end
51
81
  end
52
82
  end
@@ -35,7 +35,7 @@ module Plurimath
35
35
  rule(:op_opener) { open_paren | op_open_unicode | op_open_paren | op_open }
36
36
  rule(:op_closer) { op_close_unicode | close_paren | op_close_paren | op_close }
37
37
 
38
- rule(:op_decimal) { str(",") | str(".") }
38
+ rule(:op_decimal) { decimal_marker | str(",") | str(".") }
39
39
  rule(:diacritics) { (char.as(:char) >> diacritics.as(:diacritics)) | char.as(:char) }
40
40
  rule(:open_paren) { (op_masked_open >> (op_closer | op_opener)).as(:open_paren) | op_masked_open }
41
41
 
@@ -242,6 +242,12 @@ module Plurimath
242
242
  end
243
243
 
244
244
  root :expression
245
+
246
+ def decimal_marker
247
+ # UnicodeMath::Parser entity-encodes input before Parslet sees it, so
248
+ # non-ASCII locale markers must match that encoded parser input.
249
+ str(Utility.string_to_html_entity(Plurimath.configuration.decimal))
250
+ end
245
251
  end
246
252
  end
247
253
  end
@@ -185,7 +185,7 @@ module Plurimath
185
185
 
186
186
  rule(decimal: simple(:decimal),
187
187
  whole: simple(:whole)) do
188
- Math::Number.new("#{decimal}#{whole.value}")
188
+ Math::Number.new(Utility.html_entity_to_unicode("#{decimal}#{whole.value}"))
189
189
  end
190
190
 
191
191
  rule(positive: simple(:positive),
@@ -2222,7 +2222,7 @@ module Plurimath
2222
2222
  rule(whole: simple(:whole),
2223
2223
  decimal: simple(:decimal),
2224
2224
  fractional: simple(:fractional)) do
2225
- Math::Number.new("#{whole.value}#{decimal}#{fractional.value}")
2225
+ Math::Number.new(Utility.html_entity_to_unicode("#{whole.value}#{decimal}#{fractional.value}"))
2226
2226
  end
2227
2227
 
2228
2228
  rule(factor: simple(:factor),
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plurimath
4
- VERSION = "0.10.7"
4
+ VERSION = "0.11.0"
5
5
  end
data/lib/plurimath.rb CHANGED
@@ -8,6 +8,11 @@ require "plurimath/xml_engine"
8
8
  module Plurimath
9
9
  autoload :Asciimath, "plurimath/asciimath"
10
10
  autoload :Cli, "plurimath/cli" unless RUBY_ENGINE == "opal"
11
+ autoload :Configuration, "plurimath/configuration"
12
+ autoload :Deprecation, "plurimath/deprecation"
13
+ autoload :Error, "plurimath/errors/error"
14
+ autoload :ConfigurationError, "plurimath/errors/configuration_error"
15
+ autoload :DeprecationError, "plurimath/errors/deprecation_error"
11
16
  autoload :Formatter, "plurimath/formatter"
12
17
  autoload :Html, "plurimath/html"
13
18
  autoload :Latex, "plurimath/latex"
@@ -28,7 +33,24 @@ module Plurimath
28
33
  Mml::V4::Configuration.adapter = adapter unless Mml::V4::Configuration.adapter
29
34
  end
30
35
 
31
- module_function :mml_adapter
36
+ def configuration
37
+ @configuration ||= Configuration.new
38
+ end
39
+
40
+ def configure
41
+ yield(configuration)
42
+ end
43
+
44
+ def with_configuration
45
+ # Swap the global config to an isolated copy so block mutations are scoped.
46
+ previous_configuration = configuration
47
+ @configuration = previous_configuration.dup
48
+ yield(configuration)
49
+ ensure
50
+ @configuration = previous_configuration
51
+ end
52
+
53
+ module_function :mml_adapter, :configuration, :configure, :with_configuration
32
54
  end
33
55
 
34
56
  default_adapter =
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.10.7
4
+ version: 0.11.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: 2026-06-09 00:00:00.000000000 Z
11
+ date: 2026-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -193,19 +193,35 @@ files:
193
193
  - lib/plurimath/asciimath/parser.rb
194
194
  - lib/plurimath/asciimath/transform.rb
195
195
  - lib/plurimath/cli.rb
196
+ - lib/plurimath/configuration.rb
197
+ - lib/plurimath/deprecation.rb
196
198
  - lib/plurimath/errors.rb
199
+ - lib/plurimath/errors/configuration_error.rb
200
+ - lib/plurimath/errors/deprecation_error.rb
201
+ - lib/plurimath/errors/error.rb
197
202
  - lib/plurimath/errors/formatter/unsupported_base.rb
203
+ - lib/plurimath/errors/formatter/unsupported_locale.rb
198
204
  - lib/plurimath/errors/invalid_type_error.rb
199
205
  - lib/plurimath/errors/omml/unsupported_node_error.rb
200
206
  - lib/plurimath/errors/parse_error.rb
207
+ - lib/plurimath/errors/parse_option_error.rb
201
208
  - lib/plurimath/formatter.rb
202
- - lib/plurimath/formatter/number_formatter.rb
203
209
  - lib/plurimath/formatter/numbers.rb
204
210
  - lib/plurimath/formatter/numbers/base.rb
211
+ - lib/plurimath/formatter/numbers/base_notation.rb
212
+ - lib/plurimath/formatter/numbers/digit_sequence.rb
213
+ - lib/plurimath/formatter/numbers/format_options.rb
205
214
  - lib/plurimath/formatter/numbers/fraction.rb
206
215
  - lib/plurimath/formatter/numbers/integer.rb
216
+ - lib/plurimath/formatter/numbers/notation_renderer.rb
217
+ - lib/plurimath/formatter/numbers/number_renderer.rb
218
+ - lib/plurimath/formatter/numbers/parts.rb
219
+ - lib/plurimath/formatter/numbers/parts_renderer.rb
220
+ - lib/plurimath/formatter/numbers/precision_resolver.rb
221
+ - lib/plurimath/formatter/numbers/sign_renderer.rb
207
222
  - lib/plurimath/formatter/numbers/significant.rb
208
- - lib/plurimath/formatter/numeric_formatter.rb
223
+ - lib/plurimath/formatter/numbers/source.rb
224
+ - lib/plurimath/formatter/numbers/symbol_resolver.rb
209
225
  - lib/plurimath/formatter/standard.rb
210
226
  - lib/plurimath/formatter/supported_locales.rb
211
227
  - lib/plurimath/html.rb
@@ -213,6 +229,7 @@ files:
213
229
  - lib/plurimath/html/parse.rb
214
230
  - lib/plurimath/html/parser.rb
215
231
  - lib/plurimath/html/transform.rb
232
+ - lib/plurimath/html/transform_utility.rb
216
233
  - lib/plurimath/latex.rb
217
234
  - lib/plurimath/latex/constants.rb
218
235
  - lib/plurimath/latex/parse.rb
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Plurimath
4
- module Formatter
5
- class NumberFormatter
6
- attr_reader :number, :data_reader
7
-
8
- DEFAULT_BASE = Numbers::Base::DEFAULT_BASE
9
- HEX_ALPHABETS = "abcdef"
10
- STRING_SYMBOLS = {
11
- dot: ".",
12
- f: "F",
13
- }.freeze
14
- DEFAULT_BASE_PREFIXES = {
15
- 2 => "0b",
16
- 8 => "0o",
17
- 10 => "",
18
- 16 => "0x",
19
- }.freeze
20
-
21
- def initialize(number, data_reader = {})
22
- @number = number
23
- @data_reader = data_reader
24
- @base = data_reader[:base] || DEFAULT_BASE
25
- unless DEFAULT_BASE_PREFIXES.key?(@base)
26
- raise UnsupportedBase.new(@base,
27
- DEFAULT_BASE_PREFIXES)
28
- end
29
-
30
- # Handle base_prefix: if explicitly provided (even as nil), use it; otherwise use default
31
- @base_prefix = if data_reader.key?(:base_prefix)
32
- data_reader[:base_prefix].to_s
33
- else
34
- DEFAULT_BASE_PREFIXES[@base]
35
- end
36
- end
37
-
38
- def format(precision: nil)
39
- data_reader[:precision] = precision || precision_from(number)
40
- int, frac, integer_format, fraction_format, signif_format = *partition_tokens(number)
41
- # FIX FOR:
42
- # NotImplementedError: String#<< not supported. Mutable String methods are not supported in Opal.
43
- result = []
44
- result << integer_format.apply(int)
45
- result << fraction_format.apply(frac, result, integer_format) # use formatted int for correct fraction formatting
46
- result = result.join
47
- result = signif_format.apply(result, integer_format, fraction_format)
48
- result = result.tr(HEX_ALPHABETS, HEX_ALPHABETS.upcase) if upcase_hex?
49
- result = pre_post_fixed(result) unless base_default?
50
- "#{prefix_symbol}#{result}"
51
- end
52
-
53
- private
54
-
55
- def upcase_hex?
56
- @base == 16 && data_reader[:hex_capital]
57
- end
58
-
59
- def prefix_symbol
60
- if number.negative?
61
- "-"
62
- elsif data_reader[:number_sign]&.to_sym == :plus
63
- "+"
64
- end
65
- end
66
-
67
- def pre_post_fixed(result)
68
- if data_reader.key?(:base_postfix)
69
- "#{result}#{data_reader[:base_postfix]}"
70
- else
71
- "#{@base_prefix}#{result}"
72
- end
73
- end
74
-
75
- def partition_tokens(number)
76
- int, fraction = parse_number(number)
77
- [
78
- int,
79
- fraction,
80
- Numbers::Integer.new(data_reader),
81
- Numbers::Fraction.new(data_reader),
82
- Numbers::Significant.new(data_reader),
83
- ]
84
- end
85
-
86
- def precision_from(number)
87
- return 0 if number.fix == number
88
-
89
- parts = number.to_s(STRING_SYMBOLS[:f]).split(STRING_SYMBOLS[:dot])
90
- parts.size == 2 ? parts[1].size : 0
91
- end
92
-
93
- def parse_number(number, options = data_reader)
94
- precision = options[:precision] || precision_from(number)
95
-
96
- abs = round_to(number, precision).abs
97
- num = if precision.zero?
98
- abs.fix.to_s(STRING_SYMBOLS[:f])
99
- else
100
- abs.round(precision).to_s(STRING_SYMBOLS[:f])
101
- end
102
- num.split(STRING_SYMBOLS[:dot])
103
- end
104
-
105
- def round_to(number, precision)
106
- factor = BigDecimal(10).power(precision)
107
- (number * factor).fix / factor
108
- end
109
-
110
- def base_default?
111
- @base == DEFAULT_BASE
112
- end
113
- end
114
- end
115
- end