@@ -373,6 +373,64 @@ def self.base_units
373373 @base_units ||= definitions . dup . select { |_ , definition | definition . base? } . keys . map { new ( _1 ) }
374374 end
375375
376+ # Coerce a string or numeric value into the configured numeric type.
377+ #
378+ # When `RubyUnits.configuration.use_bigdecimal` is true, numeric strings are
379+ # converted to BigDecimal (the caller must require 'bigdecimal').
380+ # Otherwise numeric strings are converted to Float. If the input is already
381+ # a Numeric it is returned unchanged.
382+ #
383+ # @param value [String, Numeric] the value to coerce
384+ # @return [Numeric] a Numeric instance (BigDecimal or Float) or the original Numeric
385+ # @raise [ArgumentError] if the value cannot be coerced by the underlying constructors
386+ # @example
387+ # Unit.parse_number("3.14") #=> 3.14 (Float) unless use_bigdecimal is enabled
388+ # Unit.parse_number(2) #=> 2 (unchanged)
389+ def self . parse_number ( value )
390+ return value if value . is_a? ( Numeric )
391+
392+ if RubyUnits . configuration . use_bigdecimal
393+ BigDecimal ( value )
394+ else
395+ Float ( value )
396+ end
397+ end
398+
399+ # Return an Integer when the provided numeric value is mathematically
400+ # integral; otherwise return the original numeric value.
401+ #
402+ # The method first prefers `to_int` when available (exact integer
403+ # conversion). If not available it falls back to `to_i` and compares the
404+ # converted integer to the original value. This works for Float, Rational, Complex,
405+ # BigDecimal (if loaded), and Integer.
406+ #
407+ # @param value [Numeric] the numeric value to normalize
408+ # @return [Integer, Numeric] an `Integer` when the value is integral, otherwise the original numeric
409+ # @example
410+ # Unit.normalize_to_i(2.0) #=> 2
411+ # Unit.normalize_to_i(Rational(3,1)) #=> 3
412+ # Unit.normalize_to_i(3.5) #=> 3.5
413+ # :reek:ManualDispatch
414+ def self . normalize_to_i ( value )
415+ return value unless value . is_a? ( Numeric )
416+
417+ responds_to_int = value . respond_to? ( :to_int )
418+ if responds_to_int || value . respond_to? ( :to_i )
419+ int = if responds_to_int
420+ value . to_int
421+ else
422+ value . to_i
423+ end
424+ int == value ? int : value
425+ else
426+ value
427+ end
428+ rescue RangeError
429+ # This can happen when a Complex number with a non-zero imaginary part is provided, or when value is Float::NAN or
430+ # Float::INFINITY
431+ value
432+ end
433+
376434 # Parse a string consisting of a number and a unit string
377435 # NOTE: This does not properly handle units formatted like '12mg/6ml'
378436 #
@@ -395,7 +453,7 @@ def self.parse_into_numbers_and_units(string)
395453 fractional_part = Rational ( Regexp . last_match ( 3 ) . to_i , Regexp . last_match ( 4 ) . to_i )
396454 sign * ( whole_part + fractional_part )
397455 else
398- num . to_f
456+ parse_number ( num )
399457 end ,
400458 unit . to_s . strip
401459 ]
@@ -1239,10 +1297,7 @@ def convert_to(other)
12391297 converted_value = conversion_scalar * ( source_numerator_values + target_denominator_values ) . reduce ( 1 , :* ) / ( target_numerator_values + source_denominator_values ) . reduce ( 1 , :* )
12401298 # Convert the scalar to an Integer if the result is equivalent to an
12411299 # integer
1242- if scalar_is_integer
1243- converted_as_int = converted_value . to_i
1244- converted_value = converted_as_int if converted_as_int == converted_value
1245- end
1300+ converted_value = unit_class . normalize_to_i ( converted_value )
12461301 unit_class . new ( scalar : converted_value , numerator : target_num , denominator : target_den , signature : target . signature )
12471302 end
12481303 end
@@ -1257,6 +1312,17 @@ def to_f
12571312 return_scalar_or_raise ( :to_f , Float )
12581313 end
12591314
1315+ # Convert the unit's scalar to BigDecimal. Raises if not unitless.
1316+ #
1317+ # Note: Using this method requires the BigDecimal class to be available
1318+ # (e.g., by requiring `'bigdecimal'` and `'bigdecimal/util'`).
1319+ #
1320+ # @return [BigDecimal]
1321+ # @raise [RuntimeError] when not unitless
1322+ def to_d
1323+ return_scalar_or_raise ( :to_d , BigDecimal )
1324+ end
1325+
12601326 # converts the unit back to a complex if it is unitless. Otherwise raises an exception
12611327 # @return [Complex]
12621328 # @raise [RuntimeError] when not unitless
@@ -2063,12 +2129,10 @@ def parse(passed_unit_string = "0")
20632129 if unit_string . start_with? ( COMPLEX_NUMBER )
20642130 match = unit_string . match ( COMPLEX_REGEX )
20652131 real_str , imaginary_str , unit_s = match . values_at ( :real , :imaginary , :unit )
2066- real = Float ( real_str ) if real_str
2067- imaginary = Float ( imaginary_str )
2068- real_as_int = real . to_i if real
2069- real = real_as_int if real_as_int == real
2070- imaginary_as_int = imaginary . to_i
2071- imaginary = imaginary_as_int if imaginary_as_int == imaginary
2132+ real = unit_class . parse_number ( real_str ) if real_str
2133+ imaginary = unit_class . parse_number ( imaginary_str )
2134+ real = unit_class . normalize_to_i ( real ) if real
2135+ imaginary = unit_class . normalize_to_i ( imaginary )
20722136 complex = Complex ( real || 0 , imaginary )
20732137 complex_real = complex . real
20742138 complex = complex . to_i if complex . imaginary . zero? && complex_real == complex_real . to_i
@@ -2089,17 +2153,19 @@ def parse(passed_unit_string = "0")
20892153 else
20902154 ( proper + fraction )
20912155 end
2092- rational_as_int = rational . to_int
2093- rational = rational_as_int if rational_as_int == rational
2156+ rational = unit_class . normalize_to_i ( rational )
20942157 return copy ( unit_class . new ( unit_s || 1 ) * rational )
20952158 end
20962159
20972160 match = unit_string . match ( NUMBER_REGEX )
20982161 unit_str , scalar_str = match . values_at ( :unit , :scalar )
20992162 unit = unit_class . cached . get ( unit_str )
2100- mult = scalar_str == "" ? 1.0 : scalar_str . to_f
2101- mult_as_int = mult . to_int
2102- mult = mult_as_int if mult_as_int == mult
2163+ mult = if scalar_str == "" || scalar_str . nil?
2164+ unit_class . parse_number ( "1" )
2165+ else
2166+ unit_class . parse_number ( scalar_str )
2167+ end
2168+ mult = unit_class . normalize_to_i ( mult )
21032169
21042170 if unit
21052171 copy ( unit )
@@ -2184,17 +2250,16 @@ def parse(passed_unit_string = "0")
21842250 bottom_scalar , bottom = bottom . scan ( NUMBER_UNIT_REGEX ) [ 0 ]
21852251 end
21862252
2187- @scalar = @scalar . to_f unless ! @scalar || @scalar . empty?
2253+ @scalar = unit_class . parse_number ( @scalar ) if @scalar && ! @scalar . empty?
21882254 @scalar = 1 unless @scalar . is_a? Numeric
2189- scalar_as_int = @scalar . to_int
2190- @scalar = scalar_as_int if scalar_as_int == @scalar
2255+ @scalar = unit_class . normalize_to_i ( @scalar )
21912256
2192- bottom_scalar = 1 if !bottom_scalar || bottom_scalar . empty?
2193- bottom_scalar_as_int = bottom_scalar . to_i
2194- bottom_scalar = if bottom_scalar_as_int == bottom_scalar
2195- bottom_scalar_as_int
2257+ bottom_scalar = if !bottom_scalar || bottom_scalar . empty?
2258+ 1
2259+ elsif bottom_scalar . match? ( /^ #{ INTEGER_DIGITS_REGEX } $/ )
2260+ Integer ( bottom_scalar )
21962261 else
2197- bottom_scalar . to_f
2262+ unit_class . normalize_to_i ( unit_class . parse_number ( bottom_scalar ) )
21982263 end
21992264
22002265 @scalar /= bottom_scalar
0 commit comments