blob: 4ad1d866d36d65cd7c4f7fdc4704789fc25b7c11 [file] [log] [blame]
module Sass::Script::Value
# 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 < Base
# 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<String>]
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<String>]
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
Thread.current[:sass_numeric_precision] || Thread.main[:sass_numeric_precision] || 10
end
# Sets the number of digits of precision
# For example, if this is `3`,
# `3.1415926` will be printed as `3.142`.
# The numeric precision is stored as a thread local for thread safety reasons.
# To set for all threads, be sure to set the precision on the main thread.
def self.precision=(digits)
Thread.current[:sass_numeric_precision] = digits.round
Thread.current[:sass_numeric_precision_factor] = nil
Thread.current[:sass_numeric_epsilon] = nil
end
# the precision factor used in numeric output
# it is derived from the `precision` method.
def self.precision_factor
Thread.current[:sass_numeric_precision_factor] ||= 10.0**precision
end
# Used in checking equality of floating point numbers. Any
# numbers within an `epsilon` of each other are considered functionally equal.
# The value for epsilon is one tenth of the current numeric precision.
def self.epsilon
Thread.current[:sass_numeric_epsilon] ||= 1 / (precision_factor * 10)
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 [::String, Array<::String>] See \{#numerator\_units}
# @param denominator_units [::String, Array<::String>] See \{#denominator\_units}
def initialize(value, numerator_units = NO_UNITS, denominator_units = NO_UNITS)
numerator_units = [numerator_units] if numerator_units.is_a?(::String)
denominator_units = [denominator_units] if denominator_units.is_a?(::String)
super(value)
@numerator_units = numerator_units
@denominator_units = denominator_units
@options = nil
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.
#
# {Value}
# : See {Value::Base#plus}.
#
# @param other [Value] The right-hand side of the operator
# @return [Value] 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.
#
# {Value}
# : See {Value::Base#minus}.
#
# @param other [Value] The right-hand side of the operator
# @return [Value] 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.
#
# {Value}
# : See {Value::Base#div}.
#
# @param other [Value] The right-hand side of the operator
# @return [Value] The result of the operation
def div(other)
if other.is_a? Number
res = operate(other, :/)
if original && other.original
res.original = "#{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 incompatible units
def mod(other)
if other.is_a?(Number)
operate(other, :%)
else
raise NoMethodError.new(nil, :mod)
end
end
# The SassScript `==` operation.
#
# @param other [Value] The right-hand side of the operator
# @return [Boolean] Whether this number is equal to the other object
def eq(other)
return Bool::FALSE unless other.is_a?(Sass::Script::Value::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 Bool::FALSE
end
Bool.new(basically_equal?(this.value, other.value))
end
def hash
[value, numerator_units, denominator_units].hash
end
# Hash-equality works differently than `==` equality for numbers.
# Hash-equality must be transitive, so it just compares the exact value,
# numerator units, and denominator units.
def eql?(other)
basically_equal?(value, other.value) && numerator_units == other.numerator_units &&
denominator_units == other.denominator_units
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 = {})
return original if original
value = self.class.round(self.value)
str = value.to_s
# Ruby will occasionally print in scientific notation if the number is
# small enough. That's technically valid CSS, but it's not well-supported
# and confusing.
str = ("%0.#{self.class.precision}f" % value).gsub(/0*$/, '') if str.include?('e')
# Sometimes numeric formatting will result in a decimal number with a trailing zero (x.0)
if str =~ /(.*)\.0$/
str = $1
end
# We omit a leading zero before the decimal point in compressed mode.
if @options && options[:style] == :compressed
str.sub!(/^(-)?0\./, '\1.')
end
unitless? ? str : "#{str}#{unit_str}"
end
alias_method :to_sass, :inspect
# @return [Integer] The integer value of the number
# @raise [Sass::SyntaxError] if the number isn't an integer
def to_i
super unless int?
value.to_i
end
# @return [Boolean] Whether or not this number is an integer.
def int?
basically_equal?(value % 1, 0.0)
end
# @return [Boolean] Whether or not this number has no units.
def unitless?
@numerator_units.empty? && @denominator_units.empty?
end
# Checks whether the number has the numerator unit specified.
#
# @example
# number = Sass::Script::Value::Number.new(10, "px")
# number.is_unit?("px") => true
# number.is_unit?(nil) => false
#
# @param unit [::String, nil] The unit the number should have or nil if the number
# should be unitless.
# @see Number#unitless? The unitless? method may be more readable.
def is_unit?(unit)
if unit
denominator_units.size == 0 && numerator_units.size == 1 && numerator_units.first == unit
else
unitless?
end
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<String>] The numerator units to coerce this number into.
# See {\#numerator\_units}
# @param den_units [Array<String>] 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?
value
else
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)
operate(other, :+)
true
rescue Sass::UnitConversionError
false
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
# @see Sass::Script::Number.basically_equal?
def basically_equal?(num1, num2)
self.class.basically_equal?(num1, num2)
end
# Checks whether two numbers are within an epsilon of each other.
# @return [Boolean]
def self.basically_equal?(num1, num2)
(num1 - num2).abs < epsilon
end
# @private
def self.round(num)
if num.is_a?(Float) && (num.infinite? || num.nan?)
num
elsif basically_equal?(num % 1, 0.0)
num.round
else
((num * precision_factor).round / 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|
next unless convertable?(d) && (u = @numerator_units.find(&method(:convertable?)))
@value /= conversion_factor(d, u)
@denominator_units.delete_at(i)
@numerator_units.delete_at(@numerator_units.index(u))
end
end
# This is the source data for all the unit logic. It's pre-processed to make
# it efficient to figure out whether a set of units is mutually compatible
# and what the conversion ratio is between two units.
#
# These come from http://www.w3.org/TR/2012/WD-css3-values-20120308/.
relative_sizes = [
{
'in' => Rational(1),
'cm' => Rational(1, 2.54),
'pc' => Rational(1, 6),
'mm' => Rational(1, 25.4),
'q' => Rational(1, 101.6),
'pt' => Rational(1, 72),
'px' => Rational(1, 96)
},
{
'deg' => Rational(1, 360),
'grad' => Rational(1, 400),
'rad' => Rational(1, 2 * Math::PI),
'turn' => Rational(1)
},
{
's' => Rational(1),
'ms' => Rational(1, 1000)
},
{
'Hz' => Rational(1),
'kHz' => Rational(1000)
},
{
'dpi' => Rational(1),
'dpcm' => Rational(254, 100),
'dppx' => Rational(96)
}
]
# A hash from each known unit to the set of units that it's mutually
# convertible with.
MUTUALLY_CONVERTIBLE = {}
relative_sizes.map do |values|
set = values.keys.to_set
values.keys.each {|name| MUTUALLY_CONVERTIBLE[name] = set}
end
# A two-dimensional hash from two units to the conversion ratio between
# them. Multiply `X` by `CONVERSION_TABLE[X][Y]` to convert it to `Y`.
CONVERSION_TABLE = {}
relative_sizes.each do |values|
values.each do |(name1, value1)|
CONVERSION_TABLE[name1] ||= {}
values.each do |(name2, value2)|
value = value1 / value2
CONVERSION_TABLE[name1][name2] = value.denominator == 1 ? value.to_i : value.to_f
end
end
end
def conversion_factor(from_unit, to_unit)
CONVERSION_TABLE[from_unit][to_unit]
end
def convertable?(units)
units = Array(units).to_set
return true if units.empty?
return false unless (mutually_convertible = MUTUALLY_CONVERTIBLE[units.first])
units.subset?(mutually_convertible)
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
units1 = units1.map do |u|
j = units2.index(u)
next u unless j
units2.delete_at(j)
nil
end
units1.compact!
return units1, units2
end
end
end