diff --git a/lesscpy/lessc/color.py b/lesscpy/lessc/color.py index c0da7fa..cde21b3 100644 --- a/lesscpy/lessc/color.py +++ b/lesscpy/lessc/color.py @@ -105,7 +105,7 @@ class Color(): if isinstance(s, str): s = int(s.strip('%')) rgb = colorsys.hls_to_rgb(int(h) / 360.0, l / 100.0, s / 100.0) - color = (round(c * 255) for c in rgb) + color = (utility.convergent_round(c * 255) for c in rgb) return self._rgbatohex(color) raise ValueError('Illegal color values') @@ -123,8 +123,8 @@ class Color(): if isinstance(s, str): s = int(s.strip('%')) rgb = colorsys.hls_to_rgb(int(h) / 360.0, l / 100.0, s / 100.0) - color = [float(round(c * 255)) for c in rgb] - color.append(round(float(a[:-1]) / 100.0, 2)) + color = [float(utility.convergent_round(c * 255)) for c in rgb] + color.append(utility.convergent_round(float(a[:-1]) / 100.0, 2)) return "rgba(%s,%s,%s,%s)" % tuple(color) raise ValueError('Illegal color values') @@ -139,7 +139,7 @@ class Color(): """ if color: h, l, s = self._hextohls(color) - return round(h * 360.0, 3) + return utility.convergent_round(h * 360.0, 3) raise ValueError('Illegal color values') def saturation(self, color, *args): @@ -260,7 +260,7 @@ class Color(): h = ((h * 360.0) + degree) % 360.0 h = 360.0 + h if h < 0 else h rgb = colorsys.hls_to_rgb(h / 360.0, l, s) - color = (round(c * 255) for c in rgb) + color = (utility.convergent_round(c * 255) for c in rgb) return self._rgbatohex(color) raise ValueError('Illegal color values') @@ -364,5 +364,5 @@ class Color(): hls = list(self._hextohls(color)) hls[idx] = self._clamp(getattr(hls[idx], op)(diff / 100.0)) rgb = colorsys.hls_to_rgb(*hls) - color = (round(c * 255) for c in rgb) + color = (utility.away_from_zero_round(c * 255) for c in rgb) return self._rgbatohex(color) diff --git a/lesscpy/lessc/utility.py b/lesscpy/lessc/utility.py index 33977a4..02b1d07 100644 --- a/lesscpy/lessc/utility.py +++ b/lesscpy/lessc/utility.py @@ -11,7 +11,9 @@ from __future__ import print_function import collections +import math import re +import sys def flatten(lst): @@ -243,3 +245,36 @@ def split_unit(value): """ r = re.search('^(\-?[\d\.]+)(.*)$', str(value)) return r.groups() if r else ('', '') + + +def away_from_zero_round(value, ndigits=0): + """Round half-way away from zero. + + Python2's round() method. + """ + if sys.version_info[0] >= 3: + p = 10 ** ndigits + return float(math.floor((value * p) + math.copysign(0.5, value))) / p + else: + return round(value, ndigits) + + +def convergent_round(value, ndigits=0): + """Convergent rounding. + + Round to neareas even, similar to Python3's round() method. + """ + if sys.version_info[0] < 3: + if value < 0.0: + return -convergent_round(-value) + + epsilon = 0.0000001 + integral_part, _ = divmod(value, 1) + + if abs(value - (integral_part + 0.5)) < epsilon: + if integral_part % 2.0 < epsilon: + return integral_part + else: + nearest_even = integral_part + 0.5 + return math.ceil(nearest_even) + return round(value, ndigits) diff --git a/lesscpy/plib/call.py b/lesscpy/plib/call.py index d2fe8c7..2b8bbcb 100644 --- a/lesscpy/plib/call.py +++ b/lesscpy/plib/call.py @@ -196,7 +196,7 @@ class Call(Node): str """ n, u = utility.analyze_number(value) - return utility.with_unit(int(round(float(n))), u) + return utility.with_unit(int(utility.away_from_zero_round(float(n))), u) def ceil(self, value, *args): """ Ceil number diff --git a/lesscpy/plib/expression.py b/lesscpy/plib/expression.py index 6bd27ce..d758db1 100644 --- a/lesscpy/plib/expression.py +++ b/lesscpy/plib/expression.py @@ -96,7 +96,7 @@ class Expression(Node): return str(val) + ua elif ub: return str(val) + ub - return str(val) + return repr(val) def operate(self, vala, valb, oper): """Perform operation @@ -125,7 +125,9 @@ class Expression(Node): else: ret = getattr(vala, operation)(valb) if ret is NotImplemented: - ret = getattr(valb, operation)(vala) + # __truediv__(int, float) isn't implemented, but __truediv__(float, float) is. + # __add__(int, float) is similar. Simply cast vala to float: + ret = getattr(float(vala), operation)(valb) if oper in '+-*/': try: if int(ret) == ret: diff --git a/lesscpy/test/css/calls.css b/lesscpy/test/css/calls.css index d0f6aef..cde5394 100644 --- a/lesscpy/test/css/calls.css +++ b/lesscpy/test/css/calls.css @@ -6,6 +6,8 @@ decrement: 9; rounded: 11; roundedpx: 3px; + round25: 3; + round15: 2; } #escapes { escaped: -Some::weird(#thing, y); @@ -19,7 +21,7 @@ format-url-encode: 'red is %23ff0000'; } #more { - width: 1px; + width: 2px; height: 1px; top: 50%; } diff --git a/lesscpy/test/css/calls.min.css b/lesscpy/test/css/calls.min.css index f5cb8c2..c896d45 100644 --- a/lesscpy/test/css/calls.min.css +++ b/lesscpy/test/css/calls.min.css @@ -1,6 +1,6 @@ -#standard{width:16;height:undefined("self");border-width:5;variable:11;decrement:9;rounded:11;roundedpx:3px;} +#standard{width:16;height:undefined("self");border-width:5;variable:11;decrement:9;rounded:11;roundedpx:3px;round25:3;round15:2;} #escapes{escaped:-Some::weird(#thing, y);escaped1:-Some::weird(#thing, z);eformat:rgb(32, 128, 64);} #format{format:"rgb(32, 128, 64)";format-string:"hello world";format-multiple:"hello earth 2";format-url-encode:'red is %23ff0000';} -#more{width:1px;height:1px;top:50%;} +#more{width:2px;height:1px;top:50%;} #colors{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#333333', GradientType=1);} a{background-image:linear-gradient(top,#ffffff,#333333);background-image:linear-gradient(top,#a6a6a6,#000000);} diff --git a/lesscpy/test/css/colors.css b/lesscpy/test/css/colors.css index 1f0d87d..78030de 100644 --- a/lesscpy/test/css/colors.css +++ b/lesscpy/test/css/colors.css @@ -91,7 +91,7 @@ color: #c1cdc8; color: #ffffff; } -.lighten { +.darken { color: #525252; color: #3b3b3b; color: #222222; diff --git a/lesscpy/test/css/colors.min.css b/lesscpy/test/css/colors.min.css index 839529d..b9ea18f 100644 --- a/lesscpy/test/css/colors.min.css +++ b/lesscpy/test/css/colors.min.css @@ -13,7 +13,7 @@ .saturate{color:#565454;color:#5e4c4c;color:#664444;color:#773333;color:#882222;color:#aa0000;color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#29332f;color:#243830;color:#203c31;color:#174533;color:#0d4f35;color:#005c37;} .desaturate{color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#29332f;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;} .lighten{color:#585858;color:#6f6f6f;color:#888888;color:#bbbbbb;color:#eeeeee;color:#ffffff;color:#ffffff;color:#000000;color:#ffffff;color:#ffffff;color:#2b3632;color:#404f49;color:#566c63;color:#88a096;color:#c1cdc8;color:#ffffff;} -.lighten{color:#525252;color:#3b3b3b;color:#222222;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#ffffff;color:#27302c;color:#121715;color:#000000;color:#000000;color:#000000;color:#000000;} +.darken{color:#525252;color:#3b3b3b;color:#222222;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#000000;color:#ffffff;color:#27302c;color:#121715;color:#000000;color:#000000;color:#000000;color:#000000;} .spin{color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#555555;color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#29332f;color:#29332d;color:#293332;color:#2a3329;color:#292d33;color:#2c2933;} .grayscale{color:#000000;color:#000000;color:#ffffff;color:#ffffff;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;color:#2e2e2e;} .mix{color:#7f007f;color:#7f7f7f;color:#7f9055;color:#3f00bf;color:#ff0000;color:#0000ff;} diff --git a/lesscpy/test/css/grid.css b/lesscpy/test/css/grid.css index ba266f1..67674c2 100644 --- a/lesscpy/test/css/grid.css +++ b/lesscpy/test/css/grid.css @@ -124,13 +124,13 @@ margin-left: 59.1; } .offset3 { - margin-left: 87.8; + margin-left: 87.80000000000001; } .offset4 { margin-left: 116.5; } .offset5 { - margin-left: 145.2; + margin-left: 145.20000000000002; } .offset6 { margin-left: 173.9; diff --git a/lesscpy/test/css/grid.min.css b/lesscpy/test/css/grid.min.css index 215ccc7..06c58e4 100644 --- a/lesscpy/test/css/grid.min.css +++ b/lesscpy/test/css/grid.min.css @@ -39,9 +39,9 @@ .span12,.container{width:342.7;} .offset1{margin-left:30.4;} .offset2{margin-left:59.1;} -.offset3{margin-left:87.8;} +.offset3{margin-left:87.80000000000001;} .offset4{margin-left:116.5;} -.offset5{margin-left:145.2;} +.offset5{margin-left:145.20000000000002;} .offset6{margin-left:173.9;} .offset7{margin-left:202.6;} .offset8{margin-left:231.3;} diff --git a/lesscpy/test/less/calls.less b/lesscpy/test/less/calls.less index b420bc7..8cd987a 100644 --- a/lesscpy/test/less/calls.less +++ b/lesscpy/test/less/calls.less @@ -11,6 +11,8 @@ decrement: decrement(@var); rounded: round(@r/3); roundedpx: round(10px / 3); + round25: round(2.5); + round15: round(1.5); } #escapes { escaped: e("-Some::weird(#thing, y)"); diff --git a/lesscpy/test/less/colors.less b/lesscpy/test/less/colors.less index 89b0fa2..14dccf2 100644 --- a/lesscpy/test/less/colors.less +++ b/lesscpy/test/less/colors.less @@ -98,7 +98,7 @@ color: lighten(#29332f, 60%); color: lighten(#29332f, 100%); } -.lighten { +.darken { color: darken(#555, 1%); color: darken(#555, 10%); color: darken(#555, 20%); diff --git a/lesscpy/test/testutility.py b/lesscpy/test/testutility.py index 3bf8ea1..3a2a992 100644 --- a/lesscpy/test/testutility.py +++ b/lesscpy/test/testutility.py @@ -92,6 +92,20 @@ class TestUtility(unittest.TestCase): self.assertEqual('1', test(1, None)) self.assertEqual('1', test(1,)) + def test_convergent_round(self): + test = utility.convergent_round + self.assertEqual(-4, test(-4.5)) + self.assertEqual(-4, test(-3.5)) + self.assertEqual(-2, test(-2.5)) + self.assertEqual(-2, test(-1.5)) + self.assertEqual(0, test(-0.5)) + self.assertEqual(0, test(0.5)) + self.assertEqual(2, test(1.5)) + self.assertEqual(2, test(2.5)) + self.assertEqual(3.0, test(10.0 / 3, 0)) + self.assertEqual(4, test(3.5)) + self.assertEqual(4, test(4.5)) + if __name__ == '__main__': unittest.main() diff --git a/setup.py b/setup.py index dc0e9ae..c42b00a 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from distutils.core import setup setup( name='lesscpy', - version='0.9i', + version='0.9j', description='Lesscss compiler.', author='Jóhann T Maríusson', author_email='jtm@robot.is',