PageRenderTime 62ms CodeModel.GetById 11ms app.highlight 46ms RepoModel.GetById 1ms app.codeStats 0ms

/scalate-jruby/src/main/resources/haml-3.0.25/lib/sass/script/number.rb

http://github.com/scalate/scalate
Ruby | 423 lines | 221 code | 42 blank | 160 comment | 35 complexity | f91b0721d2e64abfa39acde3e4b6d0cb MD5 | raw file
  1require 'sass/script/literal'
  2
  3module Sass::Script
  4  # A SassScript object representing a number.
  5  # SassScript numbers can have decimal values,
  6  # and can also have units.
  7  # For example, `12`, `1px`, and `10.45em`
  8  # are all valid values.
  9  #
 10  # Numbers can also have more complex units, such as `1px*em/in`.
 11  # These cannot be inputted directly in Sass code at the moment.
 12  class Number < Literal
 13    # The Ruby value of the number.
 14    #
 15    # @return [Numeric]
 16    attr_reader :value
 17
 18    # A list of units in the numerator of the number.
 19    # For example, `1px*em/in*cm` would return `["px", "em"]`
 20    # @return [Array<String>] 
 21    attr_reader :numerator_units
 22
 23    # A list of units in the denominator of the number.
 24    # For example, `1px*em/in*cm` would return `["in", "cm"]`
 25    # @return [Array<String>]
 26    attr_reader :denominator_units
 27
 28    # The original representation of this number.
 29    # For example, although the result of `1px/2px` is `0.5`,
 30    # the value of `#original` is `"1px/2px"`.
 31    #
 32    # This is only non-nil when the original value should be used as the CSS value,
 33    # as in `font: 1px/2px`.
 34    #
 35    # @return [Boolean, nil]
 36    attr_accessor :original
 37
 38    # The precision with which numbers will be printed to CSS files.
 39    # For example, if this is `1000.0`,
 40    # `3.1415926` will be printed as `3.142`.
 41    # @api public
 42    PRECISION = 1000.0
 43
 44    # @param value [Numeric] The value of the number
 45    # @param numerator_units [Array<String>] See \{#numerator\_units}
 46    # @param denominator_units [Array<String>] See \{#denominator\_units}
 47    def initialize(value, numerator_units = [], denominator_units = [])
 48      super(value)
 49      @numerator_units = numerator_units
 50      @denominator_units = denominator_units
 51      normalize!
 52    end
 53
 54    # The SassScript `+` operation.
 55    # Its functionality depends on the type of its argument:
 56    #
 57    # {Number}
 58    # : Adds the two numbers together, converting units if possible.
 59    #
 60    # {Color}
 61    # : Adds this number to each of the RGB color channels.
 62    #
 63    # {Literal}
 64    # : See {Literal#plus}.
 65    #
 66    # @param other [Literal] The right-hand side of the operator
 67    # @return [Literal] The result of the operation
 68    # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units
 69    def plus(other)
 70      if other.is_a? Number
 71        operate(other, :+)
 72      elsif other.is_a?(Color)
 73        other.plus(self)
 74      else
 75        super
 76      end
 77    end
 78
 79    # The SassScript binary `-` operation (e.g. `$a - $b`).
 80    # Its functionality depends on the type of its argument:
 81    #
 82    # {Number}
 83    # : Subtracts this number from the other, converting units if possible.
 84    #
 85    # {Literal}
 86    # : See {Literal#minus}.
 87    #
 88    # @param other [Literal] The right-hand side of the operator
 89    # @return [Literal] The result of the operation
 90    # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units
 91    def minus(other)
 92      if other.is_a? Number
 93        operate(other, :-)
 94      else
 95        super
 96      end
 97    end
 98
 99    # The SassScript unary `+` operation (e.g. `+$a`).
100    #
101    # @return [Number] The value of this number
102    def unary_plus
103      self
104    end
105
106    # The SassScript unary `-` operation (e.g. `-$a`).
107    #
108    # @return [Number] The negative value of this number
109    def unary_minus
110      Number.new(-value, numerator_units, denominator_units)
111    end
112
113    # The SassScript `*` operation.
114    # Its functionality depends on the type of its argument:
115    #
116    # {Number}
117    # : Multiplies the two numbers together, converting units appropriately.
118    #
119    # {Color}
120    # : Multiplies each of the RGB color channels by this number.
121    #
122    # @param other [Number, Color] The right-hand side of the operator
123    # @return [Number, Color] The result of the operation
124    # @raise [NoMethodError] if `other` is an invalid type
125    def times(other)
126      if other.is_a? Number
127        operate(other, :*)
128      elsif other.is_a? Color
129        other.times(self)
130      else
131        raise NoMethodError.new(nil, :times)
132      end
133    end
134
135    # The SassScript `/` operation.
136    # Its functionality depends on the type of its argument:
137    #
138    # {Number}
139    # : Divides this number by the other, converting units appropriately.
140    #
141    # {Literal}
142    # : See {Literal#div}.
143    #
144    # @param other [Literal] The right-hand side of the operator
145    # @return [Literal] The result of the operation
146    def div(other)
147      if other.is_a? Number
148        res = operate(other, :/)
149        if self.original && other.original && context != :equals
150          res.original = "#{self.original}/#{other.original}"
151        end
152        res
153      else
154        super
155      end
156    end
157
158    # The SassScript `%` operation.
159    #
160    # @param other [Number] The right-hand side of the operator
161    # @return [Number] This number modulo the other
162    # @raise [NoMethodError] if `other` is an invalid type
163    # @raise [Sass::UnitConversionError] if `other` has any units
164    def mod(other)
165      if other.is_a?(Number)
166        unless other.unitless?
167          raise Sass::UnitConversionError.new("Cannot modulo by a number with units: #{other.inspect}.")
168        end
169        operate(other, :%)
170      else
171        raise NoMethodError.new(nil, :mod)
172      end
173    end
174
175    # The SassScript `==` operation.
176    #
177    # @param other [Literal] The right-hand side of the operator
178    # @return [Boolean] Whether this number is equal to the other object
179    def eq(other)
180      return Sass::Script::Bool.new(false) unless other.is_a?(Sass::Script::Number)
181      this = self
182      begin
183        if unitless?
184          this = this.coerce(other.numerator_units, other.denominator_units)
185        else
186          other = other.coerce(numerator_units, denominator_units)
187        end
188      rescue Sass::UnitConversionError
189        return Sass::Script::Bool.new(false)
190      end
191
192      Sass::Script::Bool.new(this.value == other.value)
193    end
194
195    # The SassScript `>` operation.
196    #
197    # @param other [Number] The right-hand side of the operator
198    # @return [Boolean] Whether this number is greater than the other
199    # @raise [NoMethodError] if `other` is an invalid type
200    def gt(other)
201      raise NoMethodError.new(nil, :gt) unless other.is_a?(Number)
202      operate(other, :>)
203    end
204
205    # The SassScript `>=` operation.
206    #
207    # @param other [Number] The right-hand side of the operator
208    # @return [Boolean] Whether this number is greater than or equal to the other
209    # @raise [NoMethodError] if `other` is an invalid type
210    def gte(other)
211      raise NoMethodError.new(nil, :gte) unless other.is_a?(Number)
212      operate(other, :>=)
213    end
214
215    # The SassScript `<` operation.
216    #
217    # @param other [Number] The right-hand side of the operator
218    # @return [Boolean] Whether this number is less than the other
219    # @raise [NoMethodError] if `other` is an invalid type
220    def lt(other)
221      raise NoMethodError.new(nil, :lt) unless other.is_a?(Number)
222      operate(other, :<)
223    end
224
225    # The SassScript `<=` operation.
226    #
227    # @param other [Number] The right-hand side of the operator
228    # @return [Boolean] Whether this number is less than or equal to the other
229    # @raise [NoMethodError] if `other` is an invalid type
230    def lte(other)
231      raise NoMethodError.new(nil, :lte) unless other.is_a?(Number)
232      operate(other, :<=)
233    end
234
235    # @return [String] The CSS representation of this number
236    # @raise [Sass::SyntaxError] if this number has units that can't be used in CSS
237    #   (e.g. `px*in`)
238    def to_s
239      return original if original
240      raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") unless legal_units?
241      inspect
242    end
243
244    # Returns a readable representation of this number.
245    #
246    # This representation is valid CSS (and valid SassScript)
247    # as long as there is only one unit.
248    #
249    # @return [String] The representation
250    def inspect(opts = {})
251      "#{self.class.round(self.value)}#{unit_str}"
252    end
253    alias_method :to_sass, :inspect
254
255    # @return [Fixnum] The integer value of the number
256    # @raise [Sass::SyntaxError] if the number isn't an integer
257    def to_i
258      super unless int?
259      return value
260    end
261
262    # @return [Boolean] Whether or not this number is an integer.
263    def int?
264      value % 1 == 0.0
265    end
266
267    # @return [Boolean] Whether or not this number has no units.
268    def unitless?
269      numerator_units.empty? && denominator_units.empty?
270    end
271
272    # @return [Boolean] Whether or not this number has units that can be represented in CSS
273    #   (that is, zero or one \{#numerator\_units}).
274    def legal_units?
275      (numerator_units.empty? || numerator_units.size == 1) && denominator_units.empty?
276    end
277
278    # Returns this number converted to other units.
279    # The conversion takes into account the relationship between e.g. mm and cm,
280    # as well as between e.g. in and cm.
281    #
282    # If this number has no units, it will simply return itself
283    # with the given units.
284    #
285    # An incompatible coercion, e.g. between px and cm, will raise an error.
286    #
287    # @param num_units [Array<String>] The numerator units to coerce this number into.
288    #   See {\#numerator\_units}
289    # @param den_units [Array<String>] The denominator units to coerce this number into.
290    #   See {\#denominator\_units}
291    # @return [Number] The number with the new units
292    # @raise [Sass::UnitConversionError] if the given units are incompatible with the number's
293    #   current units
294    def coerce(num_units, den_units)
295      Number.new(if unitless?
296                   self.value
297                 else
298                   self.value * coercion_factor(self.numerator_units, num_units) /
299                     coercion_factor(self.denominator_units, den_units)
300                 end, num_units, den_units)
301    end
302
303    # @param other [Number] A number to decide if it can be compared with this number.
304    # @return [Boolean] Whether or not this number can be compared with the other.
305    def comparable_to?(other)
306      begin
307        operate(other, :+)
308        true
309      rescue Sass::UnitConversionError
310        false
311      end
312    end
313
314    # Returns a human readable representation of the units in this number.
315    # For complex units this takes the form of:
316    # numerator_unit1 * numerator_unit2 / denominator_unit1 * denominator_unit2
317    # @return [String] a string that represents the units in this number
318    def unit_str
319      rv = numerator_units.sort.join("*")
320      if denominator_units.any?
321        rv << "/"
322        rv << denominator_units.sort.join("*")
323      end
324      rv
325    end
326
327    private
328
329    # @private
330    def self.round(num)
331      if num.is_a?(Float) && (num.infinite? || num.nan?)
332        num
333      elsif num % 1 == 0.0
334        num.to_i
335      else
336        (num * PRECISION).round / PRECISION
337      end
338    end
339
340    def operate(other, operation)
341      this = self
342      if [:+, :-, :<=, :<, :>, :>=].include?(operation)
343        if unitless?
344          this = this.coerce(other.numerator_units, other.denominator_units)
345        else
346          other = other.coerce(numerator_units, denominator_units)
347        end
348      end
349      # avoid integer division
350      value = (:/ == operation) ? this.value.to_f : this.value
351      result = value.send(operation, other.value)
352
353      if result.is_a?(Numeric)
354        Number.new(result, *compute_units(this, other, operation))
355      else # Boolean op
356        Bool.new(result)
357      end
358    end
359
360    def coercion_factor(from_units, to_units)
361      # get a list of unmatched units
362      from_units, to_units = sans_common_units(from_units, to_units)
363
364      if from_units.size != to_units.size || !convertable?(from_units | to_units)
365        raise Sass::UnitConversionError.new("Incompatible units: '#{from_units.join('*')}' and '#{to_units.join('*')}'.")
366      end
367
368      from_units.zip(to_units).inject(1) {|m,p| m * conversion_factor(p[0], p[1]) }
369    end
370
371    def compute_units(this, other, operation)
372      case operation
373      when :*
374        [this.numerator_units + other.numerator_units, this.denominator_units + other.denominator_units]
375      when :/
376        [this.numerator_units + other.denominator_units, this.denominator_units + other.numerator_units]
377      else  
378        [this.numerator_units, this.denominator_units]
379      end
380    end
381
382    def normalize!
383      return if unitless?
384      @numerator_units, @denominator_units = sans_common_units(numerator_units, denominator_units)
385
386      @denominator_units.each_with_index do |d, i|
387        if convertable?(d) && (u = @numerator_units.detect(&method(:convertable?)))
388          @value /= conversion_factor(d, u)
389          @denominator_units.delete_at(i)
390          @numerator_units.delete_at(@numerator_units.index(u))
391        end
392      end
393    end
394
395    # A hash of unit names to their index in the conversion table
396    CONVERTABLE_UNITS = {"in" => 0,        "cm" => 1,    "pc" => 2,    "mm" => 3,   "pt" => 4}
397    CONVERSION_TABLE = [[ 1,                2.54,         6,            25.4,        72        ], # in
398                        [ nil,              1,            2.36220473,   10,          28.3464567], # cm
399                        [ nil,              nil,          1,            4.23333333,  12        ], # pc
400                        [ nil,              nil,          nil,          1,           2.83464567], # mm
401                        [ nil,              nil,          nil,          nil,         1         ]] # pt
402
403    def conversion_factor(from_unit, to_unit)
404      res = CONVERSION_TABLE[CONVERTABLE_UNITS[from_unit]][CONVERTABLE_UNITS[to_unit]]
405      return 1.0 / conversion_factor(to_unit, from_unit) if res.nil?
406      res
407    end
408
409    def convertable?(units)
410      Array(units).all?(&CONVERTABLE_UNITS.method(:include?))
411    end
412
413    def sans_common_units(units1, units2)
414      units2 = units2.dup
415      # Can't just use -, because we want px*px to coerce properly to px*mm
416      return units1.map do |u|
417        next u unless j = units2.index(u)
418        units2.delete_at(j)
419        nil
420      end.compact, units2
421    end
422  end
423end