From c29c7f0fe750d491ecd29f497ad752b67b627308 Mon Sep 17 00:00:00 2001 From: Kevin Olbrich Date: Tue, 30 Dec 2025 09:14:56 -0500 Subject: [PATCH 1/7] docs: clarify BigDecimal parsing and numeric helpers --- lib/ruby_units/configuration.rb | 47 ++++++++++- lib/ruby_units/unit.rb | 111 ++++++++++++++++++++------ ruby-units.gemspec | 16 ++-- spec/ruby_units/configuration_spec.rb | 37 +++++++++ spec/spec_helper.rb | 1 + 5 files changed, 179 insertions(+), 33 deletions(-) diff --git a/lib/ruby_units/configuration.rb b/lib/ruby_units/configuration.rb index 0c32e06..6e8c577 100644 --- a/lib/ruby_units/configuration.rb +++ b/lib/ruby_units/configuration.rb @@ -4,6 +4,9 @@ # It allows for the creation, conversion, and mathematical operations on physical quantities # with associated units of measurement. module RubyUnits + # Raised when a requested feature cannot be enabled because a runtime + # dependency has not been loaded by the caller. + class MissingDependencyError < StandardError; end class << self # Get or initialize the configuration # @return [Configuration] the configuration instance @@ -88,16 +91,35 @@ class Configuration # @return [Numeric] the precision to use when converting to a rational (default: 0.0001) attr_reader :default_precision + # Whether to parse numeric literals as BigDecimal when parsing unit strings. + # This is an opt-in feature because BigDecimal has different performance + # and precision characteristics compared to Float. The default is `false`. + # + # When enabled, numeric strings parsed from unit inputs will be converted + # to BigDecimal. The caller must require the BigDecimal library before + # enabling this mode (for example `require 'bigdecimal'; require 'bigdecimal/util'`). + # + # @!attribute [rw] use_bigdecimal + # @return [Boolean] whether to coerce numeric literals to BigDecimal (default: false) + attr_reader :use_bigdecimal + # Initialize configuration with keyword arguments # # @param separator [Symbol, Boolean] the separator to use (:space or :none, true/false for backward compatibility) (default: :space) # @param format [Symbol] the format to use when generating output (:rational or :exponential) (default: :rational) # @param default_precision [Numeric] the precision to use when converting to a rational (default: 0.0001) + # @param use_bigdecimal [Boolean] whether to parse numeric literals as BigDecimal when possible (default: false) # @return [Configuration] a new configuration instance - def initialize(separator: :space, format: :rational, default_precision: 0.0001) + def initialize(**opts) + separator = opts.fetch(:separator, :space) + format = opts.fetch(:format, :rational) + default_precision = opts.fetch(:default_precision, 0.0001) + use_bigdecimal = opts.fetch(:use_bigdecimal, false) + self.separator = separator self.format = format self.default_precision = default_precision + self.use_bigdecimal = use_bigdecimal end # Set the separator to use when generating output. @@ -155,5 +177,28 @@ def default_precision=(value) @default_precision = value end + + # Enable or disable BigDecimal parsing for numeric literals. + # + # To enable BigDecimal parsing, the BigDecimal library must already be + # required by the application. If you attempt to enable this option + # without requiring BigDecimal first a `MissingDependencyError` will be + # raised to make the dependency requirement explicit. + # + # @param value [Boolean] + # @return [void] + # @raise [ArgumentError] if `value` is not a boolean + # @raise [MissingDependencyError] when enabling without requiring BigDecimal first + # @example + # require 'bigdecimal' + # require 'bigdecimal/util' + # RubyUnits.configuration.use_bigdecimal = true + def use_bigdecimal=(value) + raise ArgumentError, "configuration 'use_bigdecimal' must be a boolean" unless [true, false].include?(value) + + raise MissingDependencyError, "To enable use_bigdecimal, require 'bigdecimal' and 'bigdecimal/util' before setting RubyUnits.configuration.use_bigdecimal = true" if value && !defined?(BigDecimal) + + @use_bigdecimal = value + end end end diff --git a/lib/ruby_units/unit.rb b/lib/ruby_units/unit.rb index 4cccdde..2028ec1 100644 --- a/lib/ruby_units/unit.rb +++ b/lib/ruby_units/unit.rb @@ -373,6 +373,60 @@ def self.base_units @base_units ||= definitions.dup.select { |_, definition| definition.base? }.keys.map { new(_1) } end + # Coerce a string or numeric value into the configured numeric type. + # + # When `RubyUnits.configuration.use_bigdecimal` is true, numeric strings are + # converted to BigDecimal (the caller must require 'bigdecimal'/'bigdecimal/util'). + # Otherwise numeric strings are converted to Float. If the input is already + # a Numeric it is returned unchanged. + # + # @param value [String, Numeric] the value to coerce + # @return [Numeric] a Numeric instance (BigDecimal or Float) or the original Numeric + # @raise [ArgumentError] if the value cannot be coerced by the underlying constructors + # @example + # Unit.parse_number("3.14") #=> 3.14 (Float) unless use_bigdecimal is enabled + # Unit.parse_number(2) #=> 2 (unchanged) + def self.parse_number(value) + return value if value.is_a?(Numeric) + + if RubyUnits.configuration.use_bigdecimal + BigDecimal(value.to_s) + else + Float(value) + end + end + + # Return an Integer when the provided numeric value is mathematically + # integral; otherwise return the original numeric value. + # + # The method first prefers `to_int` when available (exact integer + # conversion). If not available it falls back to `to_i` and compares the + # converted integer to the original value. This works for Float, Rational, + # BigDecimal (if loaded), and Integer. + # + # @param value [Numeric] the numeric value to normalize + # @return [Integer, Numeric] an `Integer` when the value is integral, otherwise the original numeric + # @example + # Unit.normalize_to_i(2.0) #=> 2 + # Unit.normalize_to_i(Rational(3,1)) #=> 3 + # Unit.normalize_to_i(3.5) #=> 3.5 + # :reek:ManualDispatch + def self.normalize_to_i(value) + return value unless value.is_a?(Numeric) + + if value.respond_to?(:to_int) + int = value.to_int + return int if int == value + end + + if value.respond_to?(:to_i) + int = value.to_i + return int if int == value + end + + value + end + # Parse a string consisting of a number and a unit string # NOTE: This does not properly handle units formatted like '12mg/6ml' # @@ -395,7 +449,7 @@ def self.parse_into_numbers_and_units(string) fractional_part = Rational(Regexp.last_match(3).to_i, Regexp.last_match(4).to_i) sign * (whole_part + fractional_part) else - num.to_f + parse_number(num) end, unit.to_s.strip ] @@ -1239,10 +1293,7 @@ def convert_to(other) converted_value = conversion_scalar * (source_numerator_values + target_denominator_values).reduce(1, :*) / (target_numerator_values + source_denominator_values).reduce(1, :*) # Convert the scalar to an Integer if the result is equivalent to an # integer - if scalar_is_integer - converted_as_int = converted_value.to_i - converted_value = converted_as_int if converted_as_int == converted_value - end + converted_value = unit_class.normalize_to_i(converted_value) if scalar_is_integer unit_class.new(scalar: converted_value, numerator: target_num, denominator: target_den, signature: target.signature) end end @@ -1257,6 +1308,19 @@ def to_f return_scalar_or_raise(:to_f, Float) end + # Convert the unit's scalar to BigDecimal. Raises if not unitless. + # + # Note: Using this method requires the BigDecimal class to be available + # (e.g., by requiring `'bigdecimal'` and `'bigdecimal/util'`). If BigDecimal + # is not loaded, callers should enable BigDecimal parsing via + # `RubyUnits.configuration.use_bigdecimal` and require the library beforehand. + # + # @return [BigDecimal] + # @raise [RuntimeError] when not unitless + def to_d + return_scalar_or_raise(:to_d, BigDecimal) + end + # converts the unit back to a complex if it is unitless. Otherwise raises an exception # @return [Complex] # @raise [RuntimeError] when not unitless @@ -2063,12 +2127,10 @@ def parse(passed_unit_string = "0") if unit_string.start_with?(COMPLEX_NUMBER) match = unit_string.match(COMPLEX_REGEX) real_str, imaginary_str, unit_s = match.values_at(:real, :imaginary, :unit) - real = Float(real_str) if real_str - imaginary = Float(imaginary_str) - real_as_int = real.to_i if real - real = real_as_int if real_as_int == real - imaginary_as_int = imaginary.to_i - imaginary = imaginary_as_int if imaginary_as_int == imaginary + real = unit_class.parse_number(real_str) if real_str + imaginary = unit_class.parse_number(imaginary_str) + real = unit_class.normalize_to_i(real) if real + imaginary = unit_class.normalize_to_i(imaginary) complex = Complex(real || 0, imaginary) complex_real = complex.real complex = complex.to_i if complex.imaginary.zero? && complex_real == complex_real.to_i @@ -2089,17 +2151,19 @@ def parse(passed_unit_string = "0") else (proper + fraction) end - rational_as_int = rational.to_int - rational = rational_as_int if rational_as_int == rational + rational = unit_class.normalize_to_i(rational) return copy(unit_class.new(unit_s || 1) * rational) end match = unit_string.match(NUMBER_REGEX) unit_str, scalar_str = match.values_at(:unit, :scalar) unit = unit_class.cached.get(unit_str) - mult = scalar_str == "" ? 1.0 : scalar_str.to_f - mult_as_int = mult.to_int - mult = mult_as_int if mult_as_int == mult + mult = if scalar_str == "" || scalar_str.nil? + unit_class.parse_number("1") + else + unit_class.parse_number(scalar_str) + end + mult = unit_class.normalize_to_i(mult) if unit copy(unit) @@ -2184,17 +2248,16 @@ def parse(passed_unit_string = "0") bottom_scalar, bottom = bottom.scan(NUMBER_UNIT_REGEX)[0] end - @scalar = @scalar.to_f unless !@scalar || @scalar.empty? + @scalar = unit_class.parse_number(@scalar) if @scalar && !@scalar.empty? @scalar = 1 unless @scalar.is_a? Numeric - scalar_as_int = @scalar.to_int - @scalar = scalar_as_int if scalar_as_int == @scalar + @scalar = unit_class.normalize_to_i(@scalar) - bottom_scalar = 1 if !bottom_scalar || bottom_scalar.empty? - bottom_scalar_as_int = bottom_scalar.to_i - bottom_scalar = if bottom_scalar_as_int == bottom_scalar - bottom_scalar_as_int + bottom_scalar = if !bottom_scalar || bottom_scalar.empty? + 1 + elsif bottom_scalar.match?(/^#{INTEGER_DIGITS_REGEX}$/) + Integer(bottom_scalar) else - bottom_scalar.to_f + unit_class.normalize_to_i(unit_class.parse_number(bottom_scalar)) end @scalar /= bottom_scalar diff --git a/ruby-units.gemspec b/ruby-units.gemspec index c3d561c..7b84b4f 100644 --- a/ruby-units.gemspec +++ b/ruby-units.gemspec @@ -5,17 +5,17 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "ruby_units/version" Gem::Specification.new do |spec| - spec.name = "ruby-units" - spec.version = RubyUnits::Unit::VERSION - spec.authors = ["Kevin Olbrich"] - spec.email = ["kevin.olbrich@gmail.com"] + spec.name = "ruby-units" + spec.version = RubyUnits::Unit::VERSION + spec.authors = ["Kevin Olbrich"] + spec.email = ["kevin.olbrich@gmail.com"] spec.required_rubygems_version = ">= 2.0" spec.required_ruby_version = ">= 3.2" - spec.summary = "Provides classes and methods to perform unit math and conversions" - spec.description = "Provides classes and methods to perform unit math and conversions" - spec.homepage = "https://github.com/olbrich/ruby-units" - spec.license = "MIT" + spec.summary = "Provides classes and methods to perform unit math and conversions" + spec.description = "Provides classes and methods to perform unit math and conversions" + spec.homepage = "https://github.com/olbrich/ruby-units" + spec.license = "MIT" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "https://github.com/olbrich/ruby-units" diff --git a/spec/ruby_units/configuration_spec.rb b/spec/ruby_units/configuration_spec.rb index 13ec834..9790a85 100644 --- a/spec/ruby_units/configuration_spec.rb +++ b/spec/ruby_units/configuration_spec.rb @@ -35,8 +35,36 @@ end end + describe "#use_bigdecimal" do + it "validates boolean value" do + expect { described_class.new(use_bigdecimal: :maybe) }.to raise_error(ArgumentError) + end + + it "raises when enabling without requiring bigdecimal" do + config = described_class.new + hide_const("BigDecimal") + expect { config.use_bigdecimal = true }.to raise_error(RubyUnits::MissingDependencyError, /require 'bigdecimal'/) + end + + # NOTE: bigdecimal/util is required in spec_helper.rb for all specs, + it "allows enabling when bigdecimal is required" do + config = described_class.new + config.use_bigdecimal = true + expect(config.use_bigdecimal).to be true + end + end + describe ".separator" do context "when set to :space" do + around do |example| + RubyUnits.reset + RubyUnits.configure do |config| + config.separator = :space + end + example.run + RubyUnits.reset + end + it "has a space between the scalar and the unit" do expect(RubyUnits::Unit.new("1 m").to_s).to eq "1 m" end @@ -86,6 +114,15 @@ describe ".format" do context "when set to :rational" do + around do |example| + RubyUnits.reset + RubyUnits.configure do |config| + config.format = :rational + end + example.run + RubyUnits.reset + end + it "uses rational notation" do expect(RubyUnits::Unit.new("1 m/s^2").to_s).to eq "1 m/s^2" end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a64925d..ab85bd6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,3 +18,4 @@ end require_relative "../lib/ruby-units" +require "bigdecimal/util" From 3be16b3a17f547b8484884b48b2120805817a8e4 Mon Sep 17 00:00:00 2001 From: Kevin Olbrich Date: Tue, 30 Dec 2025 09:24:28 -0500 Subject: [PATCH 2/7] docs: update BigDecimal usage instructions in configuration and unit parsing --- lib/ruby_units/configuration.rb | 6 +++--- lib/ruby_units/unit.rb | 12 +++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/ruby_units/configuration.rb b/lib/ruby_units/configuration.rb index 6e8c577..8731351 100644 --- a/lib/ruby_units/configuration.rb +++ b/lib/ruby_units/configuration.rb @@ -97,7 +97,7 @@ class Configuration # # When enabled, numeric strings parsed from unit inputs will be converted # to BigDecimal. The caller must require the BigDecimal library before - # enabling this mode (for example `require 'bigdecimal'; require 'bigdecimal/util'`). + # enabling this mode (for example `require 'bigdecimal'`). # # @!attribute [rw] use_bigdecimal # @return [Boolean] whether to coerce numeric literals to BigDecimal (default: false) @@ -191,12 +191,12 @@ def default_precision=(value) # @raise [MissingDependencyError] when enabling without requiring BigDecimal first # @example # require 'bigdecimal' - # require 'bigdecimal/util' + # require 'bigdecimal/util' # for to_d method (optional) # RubyUnits.configuration.use_bigdecimal = true def use_bigdecimal=(value) raise ArgumentError, "configuration 'use_bigdecimal' must be a boolean" unless [true, false].include?(value) - raise MissingDependencyError, "To enable use_bigdecimal, require 'bigdecimal' and 'bigdecimal/util' before setting RubyUnits.configuration.use_bigdecimal = true" if value && !defined?(BigDecimal) + raise MissingDependencyError, "To enable use_bigdecimal, require 'bigdecimal' before setting RubyUnits.configuration.use_bigdecimal = true" if value && !defined?(BigDecimal) @use_bigdecimal = value end diff --git a/lib/ruby_units/unit.rb b/lib/ruby_units/unit.rb index 2028ec1..1510003 100644 --- a/lib/ruby_units/unit.rb +++ b/lib/ruby_units/unit.rb @@ -376,7 +376,7 @@ def self.base_units # Coerce a string or numeric value into the configured numeric type. # # When `RubyUnits.configuration.use_bigdecimal` is true, numeric strings are - # converted to BigDecimal (the caller must require 'bigdecimal'/'bigdecimal/util'). + # converted to BigDecimal (the caller must require 'bigdecimal'). # Otherwise numeric strings are converted to Float. If the input is already # a Numeric it is returned unchanged. # @@ -385,12 +385,12 @@ def self.base_units # @raise [ArgumentError] if the value cannot be coerced by the underlying constructors # @example # Unit.parse_number("3.14") #=> 3.14 (Float) unless use_bigdecimal is enabled - # Unit.parse_number(2) #=> 2 (unchanged) + # Unit.parse_number(2) #=> 2 (unchanged) def self.parse_number(value) return value if value.is_a?(Numeric) if RubyUnits.configuration.use_bigdecimal - BigDecimal(value.to_s) + BigDecimal(value) else Float(value) end @@ -401,7 +401,7 @@ def self.parse_number(value) # # The method first prefers `to_int` when available (exact integer # conversion). If not available it falls back to `to_i` and compares the - # converted integer to the original value. This works for Float, Rational, + # converted integer to the original value. This works for Float, Rational, Complex, # BigDecimal (if loaded), and Integer. # # @param value [Numeric] the numeric value to normalize @@ -1311,9 +1311,7 @@ def to_f # Convert the unit's scalar to BigDecimal. Raises if not unitless. # # Note: Using this method requires the BigDecimal class to be available - # (e.g., by requiring `'bigdecimal'` and `'bigdecimal/util'`). If BigDecimal - # is not loaded, callers should enable BigDecimal parsing via - # `RubyUnits.configuration.use_bigdecimal` and require the library beforehand. + # (e.g., by requiring `'bigdecimal'` and `'bigdecimal/util'`). # # @return [BigDecimal] # @raise [RuntimeError] when not unitless From fe24618e12749d3d614037a9cbb3ac60453539e9 Mon Sep 17 00:00:00 2001 From: Kevin Olbrich Date: Tue, 30 Dec 2025 09:47:48 -0500 Subject: [PATCH 3/7] refactor: simplify normalization logic in normalize_to_i method --- lib/ruby_units/unit.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/ruby_units/unit.rb b/lib/ruby_units/unit.rb index 1510003..edf8f83 100644 --- a/lib/ruby_units/unit.rb +++ b/lib/ruby_units/unit.rb @@ -414,17 +414,17 @@ def self.parse_number(value) def self.normalize_to_i(value) return value unless value.is_a?(Numeric) - if value.respond_to?(:to_int) - int = value.to_int - return int if int == value - end - - if value.respond_to?(:to_i) - int = value.to_i - return int if int == value + responds_to_int = value.respond_to?(:to_int) + if responds_to_int || value.respond_to?(:to_i) + int = if responds_to_int + value.to_int + else + value.to_i + end + int if int == value + else + value end - - value end # Parse a string consisting of a number and a unit string From 7c4eb2b70b97b268623ce0a65b5a036a9923a537 Mon Sep 17 00:00:00 2001 From: Kevin Olbrich Date: Tue, 30 Dec 2025 10:16:59 -0500 Subject: [PATCH 4/7] test: add BigDecimal parsing specifications --- .rubocop.yml | 2 ++ .rubocop_todo.yml | 19 ++-------- lib/ruby_units/unit.rb | 2 +- spec/ruby_units/bigdecimal_parsing_spec.rb | 40 ++++++++++++++++++++++ 4 files changed, 45 insertions(+), 18 deletions(-) create mode 100644 spec/ruby_units/bigdecimal_parsing_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 8e9abbb..67ea7e7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -32,3 +32,5 @@ Metrics/AbcSize: Enabled: false Layout/ExtraSpacing: AllowForAlignment: false +RSpec/DescribeClass: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 61b555c..04842ed 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-12-28 23:46:28 UTC using RuboCop version 1.82.1. +# on 2025-12-30 15:16:10 UTC using RuboCop version 1.82.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -42,21 +42,6 @@ RSpec/ContextWording: - 'spec/ruby_units/unit_spec.rb' - 'spec/ruby_units/utf-8/unit_spec.rb' -# Offense count: 13 -# Configuration parameters: IgnoredMetadata. -RSpec/DescribeClass: - Exclude: - - '**/spec/features/**/*' - - '**/spec/requests/**/*' - - '**/spec/routing/**/*' - - '**/spec/system/**/*' - - '**/spec/views/**/*' - - 'spec/ruby_units/bugs_spec.rb' - - 'spec/ruby_units/definition_spec.rb' - - 'spec/ruby_units/initialization_spec.rb' - - 'spec/ruby_units/temperature_spec.rb' - - 'spec/ruby_units/unit_spec.rb' - # Offense count: 1 RSpec/DescribeMethod: Exclude: @@ -93,7 +78,7 @@ RSpec/MultipleDescribes: Exclude: - 'spec/ruby_units/unit_spec.rb' -# Offense count: 30 +# Offense count: 33 RSpec/MultipleExpectations: Max: 6 diff --git a/lib/ruby_units/unit.rb b/lib/ruby_units/unit.rb index edf8f83..6f99bef 100644 --- a/lib/ruby_units/unit.rb +++ b/lib/ruby_units/unit.rb @@ -421,7 +421,7 @@ def self.normalize_to_i(value) else value.to_i end - int if int == value + int == value ? int : value else value end diff --git a/spec/ruby_units/bigdecimal_parsing_spec.rb b/spec/ruby_units/bigdecimal_parsing_spec.rb new file mode 100644 index 0000000..db33d27 --- /dev/null +++ b/spec/ruby_units/bigdecimal_parsing_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "Parsing with BigDecimal enabled" do + around do |example| + RubyUnits.reset + RubyUnits.configure do |config| + config.use_bigdecimal = true + end + example.run + RubyUnits.reset + end + + it "parses decimal strings into BigDecimal" do + u = RubyUnits::Unit.new("0.1 m") + expect(u.scalar).to be_a(BigDecimal) + expect(u.scalar).to eq(BigDecimal("0.1")) + end + + it "converts integral BigDecimal to Integer when appropriate" do + expect(RubyUnits::Unit.new("1.0").scalar).to be(1) + end + + it "parses scientific notation into BigDecimal" do + u = RubyUnits::Unit.new("1e-1 m") + expect(u.scalar).to be_a(BigDecimal) + expect(u.scalar).to eq(BigDecimal("0.1")) + end + + it "parses plain integers as Integer" do + expect(RubyUnits::Unit.new("1 m").scalar).to be(Integer(1)) + end + + it "parses plain floats as BigDecimal" do + u = RubyUnits::Unit.new("3.5 g") + expect(u.scalar).to be_a(BigDecimal) + expect(u.convert_to("mg").scalar).to eq(BigDecimal("3500")) + end +end From 39c0f27b80dfc93c223839b3221a8817ba45ce87 Mon Sep 17 00:00:00 2001 From: Kevin Olbrich Date: Tue, 30 Dec 2025 10:36:32 -0500 Subject: [PATCH 5/7] docs: enhance initialization documentation for configuration options --- lib/ruby_units/configuration.rb | 41 +++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/lib/ruby_units/configuration.rb b/lib/ruby_units/configuration.rb index 8731351..3731e6e 100644 --- a/lib/ruby_units/configuration.rb +++ b/lib/ruby_units/configuration.rb @@ -103,12 +103,43 @@ class Configuration # @return [Boolean] whether to coerce numeric literals to BigDecimal (default: false) attr_reader :use_bigdecimal - # Initialize configuration with keyword arguments + # Initialize configuration with keyword arguments. + # + # Accepts keyword options to set initial configuration values. Each value + # is validated by the corresponding setter method; invalid values will + # raise an error (see @raise tags below). Boolean values for + # `separator` are accepted for backward compatibility but will emit a + # deprecation warning. + # + # @param opts [Hash] the keyword options hash + # @option opts [Symbol, Boolean] :separator One of `:space` or `:none`. + # Boolean `true`/`false` are accepted for backward compatibility + # (`true` -> `:space`, `false` -> `:none`) and will emit a deprecation + # warning. Internally a `:space` separator is stored as a single space + # string (" ") and `:none` is stored as `nil`. Default: `:space`. + # @option opts [Symbol] :format The output format, one of `:rational` or + # `:exponential`. Default: `:rational`. + # @option opts [Numeric] :default_precision Positive numeric precision + # used when rationalizing fractional values. Default: `0.0001`. + # @option opts [Boolean] :use_bigdecimal When `true`, numeric literals + # parsed from unit input strings will be coerced to `BigDecimal`. + # The caller must require the BigDecimal library before enabling this + # option. Default: `false`. + # + # @raise [ArgumentError] If any provided value fails validation (invalid + # `separator`, invalid `format`, non-positive `default_precision`, or + # non-boolean `use_bigdecimal`). + # @raise [MissingDependencyError] If `use_bigdecimal` is enabled but the + # `BigDecimal` library has not been required. + # + # @example + # Configuration.new( + # separator: :none, + # format: :exponential, + # default_precision: 1e-6, + # use_bigdecimal: false + # ) # - # @param separator [Symbol, Boolean] the separator to use (:space or :none, true/false for backward compatibility) (default: :space) - # @param format [Symbol] the format to use when generating output (:rational or :exponential) (default: :rational) - # @param default_precision [Numeric] the precision to use when converting to a rational (default: 0.0001) - # @param use_bigdecimal [Boolean] whether to parse numeric literals as BigDecimal when possible (default: false) # @return [Configuration] a new configuration instance def initialize(**opts) separator = opts.fetch(:separator, :space) From 11bff0c440e61a0fd8b0ca4241981075631aec5e Mon Sep 17 00:00:00 2001 From: Kevin Olbrich Date: Tue, 30 Dec 2025 12:03:41 -0500 Subject: [PATCH 6/7] fix: update log10 and log methods to use scalar values for RubyUnits::Unit --- lib/ruby_units/math.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ruby_units/math.rb b/lib/ruby_units/math.rb index 6bf8187..2b20690 100644 --- a/lib/ruby_units/math.rb +++ b/lib/ruby_units/math.rb @@ -234,7 +234,7 @@ def atan2(x, y) # :reek:UncommunicativeMethodName def log10(number) if number.is_a?(RubyUnits::Unit) - super(number.to_f) + super(number.scalar) else super end @@ -251,10 +251,10 @@ def log10(number) # @example # Math.log(Unit.new("2.718")) #=> ~1.0 (natural log) # Math.log(Unit.new("8"), 2) #=> 3.0 (log base 2) - # Math.log(Math::E) #=> 1.0 + # Math.log(Math::E) #=> 1.0 def log(number, base = ::Math::E) if number.is_a?(RubyUnits::Unit) - super(number.to_f, base) + super(number.scalar, base) else super end From 7cfe4d07761256f16ab61afdfbf25ebbe40d76ae Mon Sep 17 00:00:00 2001 From: Kevin Olbrich Date: Tue, 30 Dec 2025 13:16:12 -0500 Subject: [PATCH 7/7] fix: handle RangeError in unit conversion and ensure proper normalization --- lib/ruby_units/unit.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/ruby_units/unit.rb b/lib/ruby_units/unit.rb index 6f99bef..0155025 100644 --- a/lib/ruby_units/unit.rb +++ b/lib/ruby_units/unit.rb @@ -425,6 +425,10 @@ def self.normalize_to_i(value) else value end + rescue RangeError + # This can happen when a Complex number with a non-zero imaginary part is provided, or when value is Float::NAN or + # Float::INFINITY + value end # Parse a string consisting of a number and a unit string @@ -1293,7 +1297,7 @@ def convert_to(other) converted_value = conversion_scalar * (source_numerator_values + target_denominator_values).reduce(1, :*) / (target_numerator_values + source_denominator_values).reduce(1, :*) # Convert the scalar to an Integer if the result is equivalent to an # integer - converted_value = unit_class.normalize_to_i(converted_value) if scalar_is_integer + converted_value = unit_class.normalize_to_i(converted_value) unit_class.new(scalar: converted_value, numerator: target_num, denominator: target_den, signature: target.signature) end end