lib/unit_measurements/measurement.rb in unit_measurements-4.9.0 vs lib/unit_measurements/measurement.rb in unit_measurements-4.10.0

- old
+ new

@@ -1,56 +1,180 @@ # -*- encoding: utf-8 -*- # -*- frozen_string_literal: true -*- # -*- warn_indent: true -*- module UnitMeasurements + # @abstract + # The +UnitMeasurements::Measurement+ is the abstract class and serves as superclass + # for all the unit groups. It includes several modules that provide mathematical + # operations, comparison, conversion, formatting, and other functionalities. + # + # This class provides a comprehensive set of methods and functionality for working + # with measurements in different units. It includes robust error handling and + # supports conversion between units. Additionally, it ensures that measurements + # are consistently represented. + # + # You should not directly initialize a +Measurement+ instance. Instead, create + # specialized measurement types by subclassing +Measurement+ and providing + # specific units and conversions through the +build+ method defined in the + # +UnitMeasurements+ module. + # + # @see Arithmetic + # @see Comparison + # @see Conversion + # @see Formatter + # @see Math + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 class Measurement include Arithmetic include Comparison include Conversion include Formatter include Math + # Regular expression to match conversion strings. CONVERSION_STRING_REGEXP = /(.+?)\s?(?:\s+(?:in|to|as)\s+(.+)|\z)/i.freeze - attr_reader :quantity, :unit + # Quantity of the measurement. + attr_reader :quantity + # The unit associated with the measurement. + attr_reader :unit + + # Initializes a new instance of +Measurement+ with a specified +quantity+ + # and +unit+. + # + # This method is intended to be overridden by subclasses and serves as a + # placeholder for common initialization logic. It raises an error if called + # directly on the abstract +Measurement+ class. + # + # @example Initializing the measurement with scientific number and unit: + # UnitMeasurements::Length.new(BigDecimal(2), "km") + # => 2.0 km + # + # UnitMeasurements::Length.new("2e+2", "km") + # => 200.0 km + # + # @example Initializing the measurement with complex number and unit: + # UnitMeasurements::Length.new(Complex(2, 3), "km") + # => 2+3i km + # + # UnitMeasurements::Length.new("2+3i", "km") + # => 2.0+3.0i km + # + # @example Initializing the measurement with rational or mixed rational number and unit: + # UnitMeasurements::Length.new(Rational(2, 3), "km") + # => 0.6666666666666666 km + # + # UnitMeasurements::Length.new(2/3r, "km") + # => 2/3 km + # + # UnitMeasurements::Length.new("2/3", "km") + # => 0.6666666666666666 km + # + # UnitMeasurements::Length.new("½", "km") + # => 0.5 km + # + # @example Initializing the measurement with ratio and unit: + # UnitMeasurements::Length.new("1:2", "km") + # => 0.5 km + # + # @param [Numeric|String] quantity The quantity of the measurement. + # @param [String|Unit] unit The unit of the measurement. + # + # @raise [BaseError] If +quantity+ or +unit+ is blank. + # + # @see BaseError + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 def initialize(quantity, unit) raise BaseError, "Quantity cannot be blank." if quantity.blank? raise BaseError, "Unit cannot be blank." if unit.blank? @quantity = convert_quantity(quantity) @unit = unit_from_unit_or_name!(unit) end + # Converts the measurement to a +target_unit+. + # + # @example + # UnitMeasurements::Length.new(1, "m").convert_to("cm") + # => 100.0 cm + # + # @param [String|Symbol] target_unit The target unit for conversion. + # + # @return [Measurement] + # A new +Measurement+ instance with the converted +quantity+ and + # +target unit+. + # + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 def convert_to(target_unit) target_unit = unit_from_unit_or_name!(target_unit) return self if target_unit == unit conversion_factor = (unit.conversion_factor / target_unit.conversion_factor) self.class.new((quantity * conversion_factor), target_unit) end - [:to, :in, :as].each { |method_alias| alias_method method_alias, :convert_to } + alias_method :to, :convert_to + alias_method :in, :convert_to + alias_method :as, :convert_to + # Converts the measurement to a +target_unit+ and updates the current instance. + # + # @example + # UnitMeasurements::Length.new(1, "m").convert_to!("cm") + # => 100.0 cm + # + # @param [String|Symbol] target_unit The target unit for conversion. + # + # @return [Measurement] + # The current +Measurement+ instance with updated +quantity+ and +unit+. + # + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 def convert_to!(target_unit) measurement = convert_to(target_unit) @quantity, @unit = measurement.quantity, measurement.unit self end - [:to!, :in!, :as!].each { |method_alias| alias_method method_alias, :convert_to! } + alias_method :to!, :convert_to! + alias_method :in!, :convert_to! + alias_method :as!, :convert_to! + # Returns an object representation of the +Measurement+. + # + # @param [TrueClass|FalseClass] dump If +true+, returns the dump representation. + # + # @return [Object] An object representation of the +Measurement+. + # + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 def inspect(dump: false) dump ? super() : to_s end + # Returns a string representation of the +Measurement+. + # + # @return [String] A string representation of the +Measurement+. + # + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 def to_s "#{quantity} #{unit}" end + # Returns the +quantity+ of the +measurement+. + # + # @return [Numeric] Quantity of the measurement. + # + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.5.0 def quantity case @quantity when Rational @quantity.denominator == 1 ? @quantity.numerator : @quantity else @@ -59,36 +183,145 @@ end class << self extend Forwardable - def unit_group - raise BaseError, "`Measurement` does not have a `unit_group` object. You cannot directly use `Measurement`. Instead, build a new unit group by calling `UnitMeasurements.build`." - end - + # Methods delegated from the unit group. def_delegators :unit_group, :primitive, :units, :unit_names, :unit_with_name_and_aliases, :unit_names_with_aliases, :unit_for, :unit_for!, :defined?, :unit_or_alias?, :[] + # Parses an input string and returns a +Measurement+ instance depending on + # the input string. This method first normalizes the +input+ internally, + # using the +Normalizer+ before parsing it using the +Parser+. + # + # If only the source unit is provided, it returns a new +Measurement+ + # instance with the quantity in the source unit.If both source and target + # units are provided in the input string, it returns a new +Measurement+ + # instance with the quantity converted to the target unit. + # + # @example Parsing string representing a complex number and source unit: + # UnitMeasurements::Length.parse("2+3i km") + # => 2.0+3.0i km + # + # @example Parsing string representing a complex number, source, and target units: + # UnitMeasurements::Length.parse("2+3i km to m") + # => 2000.0+3000.0i m + # + # @example Parsing string representing a rational or mixed rational number and source unit: + # UnitMeasurements::Length.parse("½ km") + # => 0.5 km + # + # UnitMeasurements::Length.parse("2/3 km") + # => 0.666666666666667 km + # + # UnitMeasurements::Length.parse("2 ½ km") + # => 2.5 km + # + # UnitMeasurements::Length.parse("2 1/2 km") + # => 2.5 km + # + # @example Parsing string representing a rational or mixed rational number, source, and target units: + # UnitMeasurements::Length.parse("½ km to m") + # => 500.0 km + # + # UnitMeasurements::Length.parse("2/3 km to m") + # => 666.666666666667 m + # + # UnitMeasurements::Length.parse("2 ½ km to m") + # => 2500.0 m + # + # UnitMeasurements::Length.parse("2 1/2 km to m") + # => 2500.0 m + # + # @example Parsing string representing a scientific number and source unit: + # UnitMeasurements::Length.parse("2e² km") + # => 200.0 km + # + # UnitMeasurements::Length.parse("2e+2 km") + # => 200.0 km + # + # UnitMeasurements::Length.parse("2e⁻² km") + # => 0.02 km + # + # @example Parsing string representing a scientific number, source, and target units: + # + # UnitMeasurements::Length.parse("2e+2 km to m") + # => 200000.0 m + # + # UnitMeasurements::Length.parse("2e⁻² km to m") + # => 20.0 m + # + # @example Parsing string representing a ratio and source unit: + # UnitMeasurements::Length.parse("1:2 km") + # => 0.5 km + # + # @example Parsing string representing a ratio, source, and target units: + # UnitMeasurements::Length.parse("1:2 km to m") + # => 500.0 m + # + # @param [String] input The input string to be parsed. + # + # @return [Measurement] The +Measurement+ instance. + # + # @see Parser + # @see Normalizer + # @see CONVERSION_STRING_REGEXP + # @see _parse + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 def parse(input) input = Normalizer.normalize(input) source, target = input.match(CONVERSION_STRING_REGEXP)&.captures target ? _parse(source).convert_to(target) : _parse(source) end private + # @private + # The class attribute representing an instance of +UnitGroup+. + # + # @return [UnitGroup] An instance of +UnitGroup+. + # + # @raise [BaseError] + # If directly invoked on +Measurement+ rather than its subclasses. + # + # @see UnitGroup + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 + def unit_group + raise BaseError, "`Measurement` does not have a `unit_group` instance. You cannot directly use `Measurement`. Instead, build a new unit group by calling `UnitMeasurements.build`." + end + + # @private + # Parses the normalized string to return the +Measurement+ instance. + # + # @param [String] string String to be parsed. + # + # @return [Measurement] The +Measurement+ instance. + # + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 def _parse(string) quantity, unit = Parser.parse(string) new(quantity, unit) end end private + # @private + # Converts the measurement quantity to a suitable format for internal use. + # + # @param [Numeric|String] quantity The quantity of the measurement. + # + # @return [Numeric] The converted quantity. + # + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 def convert_quantity(quantity) case quantity when Float BigDecimal(quantity, Float::DIG) when Integer @@ -101,10 +334,20 @@ else quantity end end + # @private + # Returns the +Unit+ instance associated with the +value+ provided. + # + # @param [String|Unit] value + # The value representing a unit name or +Unit+ instance. + # + # @return [Unit] The +Unit+ instance associated with +value+. + # + # @author {Harshal V. Ladhe}[https://shivam091.github.io/] + # @since 1.0.0 def unit_from_unit_or_name!(value) - value.is_a?(Unit) ? value : self.class.unit_group.unit_for!(value) + value.is_a?(Unit) ? value : self.class.send(:unit_group).unit_for!(value) end end end