require 'sass/script/literal' module Sass::Script # A SassScript object representing a number. # SassScript numbers can have decimal values, # and can also have units. # For example, `12`, `1px`, and `10.45em` # are all valid values. # # Numbers can also have more complex units, such as `1px*em/in`. # These cannot be inputted directly in Sass code at the moment. class Number < Literal # The Ruby value of the number. # # @return [Numeric] attr_reader :value # A list of units in the numerator of the number. # For example, `1px*em/in*cm` would return `["px", "em"]` # @return [Array] attr_reader :numerator_units # A list of units in the denominator of the number. # For example, `1px*em/in*cm` would return `["in", "cm"]` # @return [Array] attr_reader :denominator_units # The original representation of this number. # For example, although the result of `1px/2px` is `0.5`, # the value of `#original` is `"1px/2px"`. # # This is only non-nil when the original value should be used as the CSS value, # as in `font: 1px/2px`. # # @return [Boolean, nil] attr_accessor :original def self.precision @precision ||= 3 end # Sets the number of digits of precision # For example, if this is `3`, # `3.1415926` will be printed as `3.142`. def self.precision=(digits) @precision = digits.round @precision_factor = 10.0**@precision end # the precision factor used in numeric output # it is derived from the `precision` method. def self.precision_factor @precision_factor ||= 10.0**precision end # Handles the deprecation warning for the PRECISION constant # This can be removed in 3.2. def self.const_missing(const) if const == :PRECISION Sass::Util.sass_warn("Sass::Script::Number::PRECISION is deprecated and will be removed in a future release. Use Sass::Script::Number.precision_factor instead.") const_set(:PRECISION, self.precision_factor) else super end end # Used so we don't allocate two new arrays for each new number. NO_UNITS = [] # @param value [Numeric] The value of the number # @param numerator_units [Array] See \{#numerator\_units} # @param denominator_units [Array] See \{#denominator\_units} def initialize(value, numerator_units = NO_UNITS, denominator_units = NO_UNITS) super(value) @numerator_units = numerator_units @denominator_units = denominator_units normalize! end # The SassScript `+` operation. # Its functionality depends on the type of its argument: # # {Number} # : Adds the two numbers together, converting units if possible. # # {Color} # : Adds this number to each of the RGB color channels. # # {Literal} # : See {Literal#plus}. # # @param other [Literal] The right-hand side of the operator # @return [Literal] The result of the operation # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units def plus(other) if other.is_a? Number operate(other, :+) elsif other.is_a?(Color) other.plus(self) else super end end # The SassScript binary `-` operation (e.g. `$a - $b`). # Its functionality depends on the type of its argument: # # {Number} # : Subtracts this number from the other, converting units if possible. # # {Literal} # : See {Literal#minus}. # # @param other [Literal] The right-hand side of the operator # @return [Literal] The result of the operation # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units def minus(other) if other.is_a? Number operate(other, :-) else super end end # The SassScript unary `+` operation (e.g. `+$a`). # # @return [Number] The value of this number def unary_plus self end # The SassScript unary `-` operation (e.g. `-$a`). # # @return [Number] The negative value of this number def unary_minus Number.new(-value, @numerator_units, @denominator_units) end # The SassScript `*` operation. # Its functionality depends on the type of its argument: # # {Number} # : Multiplies the two numbers together, converting units appropriately. # # {Color} # : Multiplies each of the RGB color channels by this number. # # @param other [Number, Color] The right-hand side of the operator # @return [Number, Color] The result of the operation # @raise [NoMethodError] if `other` is an invalid type def times(other) if other.is_a? Number operate(other, :*) elsif other.is_a? Color other.times(self) else raise NoMethodError.new(nil, :times) end end # The SassScript `/` operation. # Its functionality depends on the type of its argument: # # {Number} # : Divides this number by the other, converting units appropriately. # # {Literal} # : See {Literal#div}. # # @param other [Literal] The right-hand side of the operator # @return [Literal] The result of the operation def div(other) if other.is_a? Number res = operate(other, :/) if self.original && other.original res.original = "#{self.original}/#{other.original}" end res else super end end # The SassScript `%` operation. # # @param other [Number] The right-hand side of the operator # @return [Number] This number modulo the other # @raise [NoMethodError] if `other` is an invalid type # @raise [Sass::UnitConversionError] if `other` has any units def mod(other) if other.is_a?(Number) unless other.unitless? raise Sass::UnitConversionError.new("Cannot modulo by a number with units: #{other.inspect}.") end operate(other, :%) else raise NoMethodError.new(nil, :mod) end end # The SassScript `==` operation. # # @param other [Literal] The right-hand side of the operator # @return [Boolean] Whether this number is equal to the other object def eq(other) return Sass::Script::Bool.new(false) unless other.is_a?(Sass::Script::Number) this = self begin if unitless? this = this.coerce(other.numerator_units, other.denominator_units) else other = other.coerce(@numerator_units, @denominator_units) end rescue Sass::UnitConversionError return Sass::Script::Bool.new(false) end Sass::Script::Bool.new(this.value == other.value) end # The SassScript `>` operation. # # @param other [Number] The right-hand side of the operator # @return [Boolean] Whether this number is greater than the other # @raise [NoMethodError] if `other` is an invalid type def gt(other) raise NoMethodError.new(nil, :gt) unless other.is_a?(Number) operate(other, :>) end # The SassScript `>=` operation. # # @param other [Number] The right-hand side of the operator # @return [Boolean] Whether this number is greater than or equal to the other # @raise [NoMethodError] if `other` is an invalid type def gte(other) raise NoMethodError.new(nil, :gte) unless other.is_a?(Number) operate(other, :>=) end # The SassScript `<` operation. # # @param other [Number] The right-hand side of the operator # @return [Boolean] Whether this number is less than the other # @raise [NoMethodError] if `other` is an invalid type def lt(other) raise NoMethodError.new(nil, :lt) unless other.is_a?(Number) operate(other, :<) end # The SassScript `<=` operation. # # @param other [Number] The right-hand side of the operator # @return [Boolean] Whether this number is less than or equal to the other # @raise [NoMethodError] if `other` is an invalid type def lte(other) raise NoMethodError.new(nil, :lte) unless other.is_a?(Number) operate(other, :<=) end # @return [String] The CSS representation of this number # @raise [Sass::SyntaxError] if this number has units that can't be used in CSS # (e.g. `px*in`) def to_s(opts = {}) return original if original raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") unless legal_units? inspect end # Returns a readable representation of this number. # # This representation is valid CSS (and valid SassScript) # as long as there is only one unit. # # @return [String] The representation def inspect(opts = {}) value = self.class.round(self.value) unitless? ? value.to_s : "#{value}#{unit_str}" end alias_method :to_sass, :inspect # @return [Fixnum] The integer value of the number # @raise [Sass::SyntaxError] if the number isn't an integer def to_i super unless int? return value end # @return [Boolean] Whether or not this number is an integer. def int? value % 1 == 0.0 end # @return [Boolean] Whether or not this number has no units. def unitless? @numerator_units.empty? && @denominator_units.empty? end # @return [Boolean] Whether or not this number has units that can be represented in CSS # (that is, zero or one \{#numerator\_units}). def legal_units? (@numerator_units.empty? || @numerator_units.size == 1) && @denominator_units.empty? end # Returns this number converted to other units. # The conversion takes into account the relationship between e.g. mm and cm, # as well as between e.g. in and cm. # # If this number has no units, it will simply return itself # with the given units. # # An incompatible coercion, e.g. between px and cm, will raise an error. # # @param num_units [Array] The numerator units to coerce this number into. # See {\#numerator\_units} # @param den_units [Array] The denominator units to coerce this number into. # See {\#denominator\_units} # @return [Number] The number with the new units # @raise [Sass::UnitConversionError] if the given units are incompatible with the number's # current units def coerce(num_units, den_units) Number.new(if unitless? self.value else self.value * coercion_factor(@numerator_units, num_units) / coercion_factor(@denominator_units, den_units) end, num_units, den_units) end # @param other [Number] A number to decide if it can be compared with this number. # @return [Boolean] Whether or not this number can be compared with the other. def comparable_to?(other) begin operate(other, :+) true rescue Sass::UnitConversionError false end end # Returns a human readable representation of the units in this number. # For complex units this takes the form of: # numerator_unit1 * numerator_unit2 / denominator_unit1 * denominator_unit2 # @return [String] a string that represents the units in this number def unit_str rv = @numerator_units.sort.join("*") if @denominator_units.any? rv << "/" rv << @denominator_units.sort.join("*") end rv end private # @private def self.round(num) if num.is_a?(Float) && (num.infinite? || num.nan?) num elsif num % 1 == 0.0 num.to_i else ((num * self.precision_factor).round / self.precision_factor).to_f end end OPERATIONS = [:+, :-, :<=, :<, :>, :>=] def operate(other, operation) this = self if OPERATIONS.include?(operation) if unitless? this = this.coerce(other.numerator_units, other.denominator_units) else other = other.coerce(@numerator_units, @denominator_units) end end # avoid integer division value = (:/ == operation) ? this.value.to_f : this.value result = value.send(operation, other.value) if result.is_a?(Numeric) Number.new(result, *compute_units(this, other, operation)) else # Boolean op Bool.new(result) end end def coercion_factor(from_units, to_units) # get a list of unmatched units from_units, to_units = sans_common_units(from_units, to_units) if from_units.size != to_units.size || !convertable?(from_units | to_units) raise Sass::UnitConversionError.new("Incompatible units: '#{from_units.join('*')}' and '#{to_units.join('*')}'.") end from_units.zip(to_units).inject(1) {|m,p| m * conversion_factor(p[0], p[1]) } end def compute_units(this, other, operation) case operation when :* [this.numerator_units + other.numerator_units, this.denominator_units + other.denominator_units] when :/ [this.numerator_units + other.denominator_units, this.denominator_units + other.numerator_units] else [this.numerator_units, this.denominator_units] end end def normalize! return if unitless? @numerator_units, @denominator_units = sans_common_units(@numerator_units, @denominator_units) @denominator_units.each_with_index do |d, i| if convertable?(d) && (u = @numerator_units.detect(&method(:convertable?))) @value /= conversion_factor(d, u) @denominator_units.delete_at(i) @numerator_units.delete_at(@numerator_units.index(u)) end end end # A hash of unit names to their index in the conversion table CONVERTABLE_UNITS = {"in" => 0, "cm" => 1, "pc" => 2, "mm" => 3, "pt" => 4} CONVERSION_TABLE = [[ 1, 2.54, 6, 25.4, 72 ], # in [ nil, 1, 2.36220473, 10, 28.3464567], # cm [ nil, nil, 1, 4.23333333, 12 ], # pc [ nil, nil, nil, 1, 2.83464567], # mm [ nil, nil, nil, nil, 1 ]] # pt def conversion_factor(from_unit, to_unit) res = CONVERSION_TABLE[CONVERTABLE_UNITS[from_unit]][CONVERTABLE_UNITS[to_unit]] return 1.0 / conversion_factor(to_unit, from_unit) if res.nil? res end def convertable?(units) Array(units).all? {|u| CONVERTABLE_UNITS.include?(u)} end def sans_common_units(units1, units2) units2 = units2.dup # Can't just use -, because we want px*px to coerce properly to px*mm return units1.map do |u| next u unless j = units2.index(u) units2.delete_at(j) nil end.compact, units2 end end end