summaryrefslogtreecommitdiff
path: root/scss
diff options
context:
space:
mode:
authorEevee (Lexy Munroe) <eevee.git@veekun.com>2016-06-08 18:43:11 -0700
committerEevee (Lexy Munroe) <eevee.git@veekun.com>2016-06-08 18:43:11 -0700
commit9ea91782b4b4dacc144454db14c32e9302e9347b (patch)
treed5eef8f47153aeba3ce3c077829b946203917ac9 /scss
parent8ded7bef22819a080299a3b0dd32fd08b8dbdff7 (diff)
downloadpyscss-9ea91782b4b4dacc144454db14c32e9302e9347b.tar.gz
Avoid precision loss when converting between units. Fixes #330
Diffstat (limited to 'scss')
-rw-r--r--scss/cssdefs.py19
-rw-r--r--scss/tests/files/bugs/unit-float-precision.css3
-rw-r--r--scss/tests/files/bugs/unit-float-precision.scss7
-rw-r--r--scss/types.py17
4 files changed, 36 insertions, 10 deletions
diff --git a/scss/cssdefs.py b/scss/cssdefs.py
index 5d9b7eb..79be7b4 100644
--- a/scss/cssdefs.py
+++ b/scss/cssdefs.py
@@ -1,6 +1,7 @@
"""Constants and functions defined by the CSS specification, not specific to
Sass.
"""
+from fractions import Fraction
from math import pi
import re
@@ -166,15 +167,15 @@ BASE_UNIT_CONVERSIONS = {
# Lengths
'mm': (1, 'mm'),
'cm': (10, 'mm'),
- 'in': (25.4, 'mm'),
- 'px': (25.4 / 96, 'mm'),
- 'pt': (25.4 / 72, 'mm'),
- 'pc': (25.4 / 6, 'mm'),
+ 'in': (Fraction(254, 10), 'mm'),
+ 'px': (Fraction(254, 960), 'mm'),
+ 'pt': (Fraction(254, 720), 'mm'),
+ 'pc': (Fraction(254, 60), 'mm'),
# Angles
- 'deg': (1 / 360, 'turn'),
- 'grad': (1 / 400, 'turn'),
- 'rad': (pi / 2, 'turn'),
+ 'deg': (Fraction(1, 360), 'turn'),
+ 'grad': (Fraction(1, 400), 'turn'),
+ 'rad': (Fraction.from_float(pi / 2), 'turn'),
'turn': (1, 'turn'),
# Times
@@ -187,7 +188,7 @@ BASE_UNIT_CONVERSIONS = {
# Resolutions
'dpi': (1, 'dpi'),
- 'dpcm': (2.54, 'dpi'),
+ 'dpcm': (Fraction(254 / 100), 'dpi'),
'dppx': (96, 'dpi'),
}
@@ -252,7 +253,7 @@ def cancel_base_units(units, to_remove):
# Copy the dict since we're about to mutate it
to_remove = to_remove.copy()
remaining_units = []
- total_factor = 1
+ total_factor = Fraction(1)
for unit in units:
factor, base_unit = get_conversion_factor(unit)
diff --git a/scss/tests/files/bugs/unit-float-precision.css b/scss/tests/files/bugs/unit-float-precision.css
new file mode 100644
index 0000000..16e9d86
--- /dev/null
+++ b/scss/tests/files/bugs/unit-float-precision.css
@@ -0,0 +1,3 @@
+a {
+ width: 702px;
+}
diff --git a/scss/tests/files/bugs/unit-float-precision.scss b/scss/tests/files/bugs/unit-float-precision.scss
new file mode 100644
index 0000000..a57b294
--- /dev/null
+++ b/scss/tests/files/bugs/unit-float-precision.scss
@@ -0,0 +1,7 @@
+// A number of pixels is actually stored internally as millimeters, which
+// causes some slight precision issues in corner cases. In this case, the
+// answer came out as 701px rather than 702px, because the subtraction yielded
+// 701.99999999px.
+a {
+ width: floor(742px - 40px);
+}
diff --git a/scss/types.py b/scss/types.py
index e733377..c35905a 100644
--- a/scss/types.py
+++ b/scss/types.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
from collections import Iterable
import colorsys
+from fractions import Fraction
import operator
import re
import string
@@ -255,7 +256,21 @@ class Number(Value):
self.unit_denom = amount.unit_denom
return
- if not isinstance(amount, (int, float)):
+ # Numbers with units are stored internally as a "base" unit, which can
+ # involve float division, which can lead to precision errors in obscure
+ # cases. Storing the original units would only partially solve this
+ # problem, because there'd still be a possible loss of precision when
+ # converting in Sass-land. Almost all of the conversion factors are
+ # simple ratios of small whole numbers, so using Fraction across the
+ # board preserves as much precision as possible.
+ # TODO in fact, i wouldn't mind parsing Sass values as fractions of a
+ # power of ten!
+ # TODO this slowed the test suite down by about 10%, ha
+ if isinstance(amount, (int, float)):
+ amount = Fraction.from_float(amount)
+ elif isinstance(amount, Fraction):
+ pass
+ else:
raise TypeError("Expected number, got %r" % (amount,))
if unit is not None: