Properly serialize numbers.

This commit is contained in:
Natalie Weizenbaum 2016-10-16 14:07:30 -07:00
parent ddf6452f20
commit c41f2bb023
4 changed files with 163 additions and 16 deletions

View File

@ -67,12 +67,28 @@ int asHex(int character) {
return 10 + character - $a;
}
/// Returns the hexadecimal digit for [character].
/// Returns the hexadecimal digit for [number].
///
/// Assumes that [character] is less than 16.
int hexCharFor(int character) {
assert(character < 0x10);
return character < 0xA ? $0 + character : $a - 0xA + character;
/// Assumes that [number] is less than 16.
int hexCharFor(int number) {
assert(number < 0x10);
return number < 0xA ? $0 + number : $a - 0xA + number;
}
/// Returns the value of [character] as a decimal digit.
///
/// Assumes that [character] is a decimal digit.
int asDecimal(int character) {
assert(character >= $0 && character <= $9);
return character - $0;
}
/// Returns the decimal digit for [number].
///
/// Assumes that [number] is less than 10.
int decimalCharFor(int number) {
assert(number < 10);
return $0 + number;
}
/// Assumes that [character] is a left-hand brace-like character, and returns

View File

@ -2,11 +2,13 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:math' as math;
import '../value.dart';
/// The maximum distance two Sass numbers are allowed to be from one another
/// before they're considered different.
const epsilon = 1 / (10 * SassNumber.precision);
final epsilon = 1 / (math.pow(10, SassNumber.precision));
/// Returns whether [number1] and [number2] are equal within [epsilon].
bool fuzzyEquals(num number1, num number2) =>
@ -31,6 +33,21 @@ bool fuzzyGreaterThan(num number1, num number2) =>
bool fuzzyGreaterThanOrEquals(num number1, num number2) =>
number1 > number2 || fuzzyEquals(number1, number2);
/// Returns whether [number] is [fuzzyEquals] to an integer.
bool fuzzyIsInt(num number) {
if (number is int) return true;
// Check against 0.5 rather than 0.0 so that we catch numbers that are both
// very slightly above an integer, and very slightly below.
return fuzzyEquals((number - 0.5).abs() % 1, 0.5);
}
/// If [number] is an integer according to [fuzzyIsInt], returns it as an
/// [int].
///
/// Otherwise, returns `null`.
int fuzzyAsInt(num number) => fuzzyIsInt(number) ? number.round() : null;
/// Rounds [number] to the nearest integer.
///
/// This rounds up numbers that are [fuzzyEquals] to `X.5`.

View File

@ -166,8 +166,16 @@ class SassNumber extends Value {
/// Whether [this] is an integer, according to [fuzzyEquals].
///
/// The [int] value can be accessed using [assertInt].
bool get isInt => value is int || fuzzyEquals(value % 1, 0.0);
/// The [int] value can be accessed using [asInt] or [assertInt]. Note that
/// this may return `false` for very large doubles even though they may be
/// mathematically integers, because not all platforms have a valid
/// representation for integers that large.
bool get isInt => fuzzyIsInt(value);
/// If [this] is an integer according to [isInt], returns [value] as an [int].
///
/// Otherwise, returns `null`.
int get asInt => fuzzyAsInt(value);
/// Returns a human readable string representation of this number's units.
String get unitString =>
@ -196,13 +204,14 @@ class SassNumber extends Value {
SassNumber assertNumber([String name]) => this;
/// Returns [value] as an [int], if it's an integer value according to
/// [fuzzyEquals].
/// [isInt].
///
/// Throws an [InternalException] if [value] isn't an integer. If this came
/// from a function argument, [name] is the argument name (without the `$`).
/// It's used for debugging.
int assertInt([String name]) {
if (isInt) return value.round();
var integer = fuzzyAsInt(value);
if (integer != null) return integer;
throw _exception("$this is not an int.", name);
}

View File

@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:charcode/charcode.dart';
import 'package:string_scanner/string_scanner.dart';
@ -11,6 +12,7 @@ import '../ast/css.dart';
import '../ast/selector.dart';
import '../exception.dart';
import '../util/character.dart';
import '../util/number.dart';
import '../value.dart';
import 'interface/css.dart';
import 'interface/selector.dart';
@ -264,10 +266,9 @@ class _SerializeCssVisitor
_writeHexComponent(value.green);
_writeHexComponent(value.blue);
} else {
// TODO: support precision in alpha, make sure we don't write exponential
// notation.
_buffer.write(
"rgba(${value.red}, ${value.green}, ${value.blue}, ${value.alpha})");
_buffer.write("rgba(${value.red}, ${value.green}, ${value.blue}, ");
_writeNumber(value.alpha);
_buffer.writeCharCode($rparen);
}
}
@ -340,9 +341,8 @@ class _SerializeCssVisitor
_buffer.write("null");
}
// TODO(nweiz): Support precision and don't support exponent notation.
void visitNumber(SassNumber value) {
_buffer.write(value.value);
_writeNumber(value.value);
if (!_inspect) {
if (value.numeratorUnits.length > 1 ||
@ -358,6 +358,111 @@ class _SerializeCssVisitor
}
}
/// Writes [number] without exponent notation and with at most
/// [SassNumber.precision] digits after the decimal point.
void _writeNumber(num number) {
// Dart always converts integers to strings in the obvious way, so all we
// have to do is clamp doubles that are close to being integers.
var integer = fuzzyAsInt(number);
if (integer != null) {
_buffer.write(integer);
return;
}
var text = number.toString();
if (text.contains("e")) text = _removeExponent(text);
// Any double that doesn't contain "e" and is less than
// `SassNumber.precision + 2` digits long is guaranteed to be safe to emit
// directly, since it'll contain at most `0.` followed by
// [SassNumber.precision] digits.
if (text.length < SassNumber.precision + 2) {
_buffer.write(text);
return;
}
_writeDecimal(text);
}
/// Assuming [text] is a double written in exponent notation, returns a string
/// representation of that double without exponent notation.
String _removeExponent(String text) {
var buffer = new StringBuffer();
int exponent;
for (var i = 0; i < text.length; i++) {
var codeUnit = text.codeUnitAt(i);
if (codeUnit == $e) {
exponent = int.parse(text.substring(i + 1, text.length));
break;
} else if (codeUnit != $dot) {
buffer.writeCharCode(codeUnit);
}
}
if (exponent > 0) {
for (var i = 0; i < exponent; i++) {
buffer.writeCharCode($0);
}
return buffer.toString();
} else {
var result = new StringBuffer();
var negative = text.codeUnitAt(0) == $minus;
if (negative) result.writeCharCode($minus);
result.write("0.");
for (var i = -1; i > exponent; i--) {
result.writeCharCode($0);
}
result.write(negative ? buffer.toString().substring(1) : buffer);
return result.toString();
}
}
/// Assuming [text] is a double written without exponent notation, writes it
/// to [_buffer] with at most [SassNumber.precision] digits after the decimal.
void _writeDecimal(String text) {
var textIndex = 0;
for (; textIndex < text.length; textIndex++) {
var codeUnit = text.codeUnitAt(textIndex);
_buffer.writeCharCode(codeUnit);
if (codeUnit == $dot) {
textIndex++;
break;
}
}
if (textIndex == text.length) return;
// We need to ensure that we write at most [SassNumber.precision] digits
// after the decimal point, and that we round appropriately if necessary. To
// do this, we maintain an intermediate buffer of decimal digits, which we
// then convert to text.
var digits = new Uint8List(SassNumber.precision);
var digitsIndex = 0;
while (textIndex < text.length && digitsIndex < digits.length) {
digits[digitsIndex++] = asDecimal(text.codeUnitAt(textIndex++));
}
// Round the trailing digits in [digits] up if necessary. We can be
// confident this won't cause us to need to round anything before the
// decimal, because otherwise the number would be [fuzzyIsInt].
if (textIndex != text.length &&
asDecimal(text.codeUnitAt(textIndex)) >= 5) {
while (digitsIndex >= 0) {
var newDigit = ++digits[digitsIndex - 1];
if (newDigit != 10) break;
digitsIndex--;
}
}
// Remove trailing zeros.
while (digitsIndex >= 0 && digits[digitsIndex - 1] == 0) {
digitsIndex--;
}
for (var i = 0; i < digitsIndex; i++) {
_buffer.writeCharCode(decimalCharFor(digits[i]));
}
}
void visitString(SassString string) {
if (string.hasQuotes) {
_visitString(string.text);