/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

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