Skip to content

Commit c730099

Browse files
committed
Serialize number to no more than necessary precision
1 parent e009794 commit c730099

File tree

5 files changed

+154
-6
lines changed

5 files changed

+154
-6
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ encoding_rs = "0.5"
2121

2222
[dependencies]
2323
cssparser-macros = {path = "./macros", version = "0.3"}
24+
dtoa = "0.4.1"
2425
heapsize = {version = ">= 0.3, < 0.5", optional = true}
2526
matches = "0.1"
2627
phf = "0.7"

src/css-parsing-tests/component_value_list.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@
247247
["number", "-0.67", -0.67, "number"]
248248
],
249249

250+
"12.3e30 -0.0123e30 9999e-13", [
251+
["number", "1.23e31", 1.23e31, "number"], " ",
252+
["number", "-1.23e28", -1.23e28, "number"], " ",
253+
["number", "9.999e-10", 9.999e-10, "number"]
254+
],
255+
250256
"3. /* Decimal point must have following digits */", [
251257
["number", "3", 3, "integer"], ".", " "
252258
],
@@ -264,12 +270,12 @@
264270
["percentage", "12", 12, "integer"], " ",
265271
["percentage", "+34", 34, "integer"], " ",
266272
["percentage", "-45", -45, "integer"], " ",
267-
["percentage", "0.66999996", 0.67, "number"], " ",
273+
["percentage", "0.67", 0.67, "number"], " ",
268274
["percentage", "+0.89", 0.89, "number"], " ",
269275
["percentage", "-0.01", -0.01, "number"], " ",
270276
["percentage", "2.3", 2.3000000000, "number"], " ",
271277
["percentage", "+45.0", 45.0, "number"], " ",
272-
["percentage", "-0.66999996", -0.67, "number"]
278+
["percentage", "-0.67", -0.67, "number"]
273279
],
274280

275281
"12e2% +34e+1% -45E-0% .68e+3% +.79e-1% -.01E2% 2.3E+1% +45.0e6% -0.67e0%", [
@@ -281,7 +287,7 @@
281287
["percentage", "-1.0", -1, "number"], " ",
282288
["percentage", "23.0", 23, "number"], " ",
283289
["percentage", "+45000000.0", 45000000, "number"], " ",
284-
["percentage", "-0.66999996", -0.67, "number"]
290+
["percentage", "-0.67", -0.67, "number"]
285291
],
286292

287293
"12\\% /* Percent sign can not be escaped */", [

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ fn parse_border_spacing(_context: &ParserContext, input: &mut Parser)
6868

6969
#![recursion_limit="200"] // For color::parse_color_keyword
7070

71+
extern crate dtoa;
7172
#[macro_use] extern crate cssparser_macros;
7273
#[macro_use] extern crate matches;
7374
#[macro_use] extern crate procedural_masquerade;

src/serializer.rs

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
use dtoa;
56
use std::ascii::AsciiExt;
67
use std::fmt::{self, Write};
8+
use std::str;
79

810
use super::Token;
911

@@ -41,6 +43,123 @@ pub trait ToCss {
4143
}
4244
}
4345

46+
const PRECISION_FOR_F32: usize = 6;
47+
48+
fn dtoa_pretty<W>(dest: &mut W, value: f32, has_exp: &mut bool)
49+
-> fmt::Result where W: fmt::Write {
50+
let mut buf = [b'\0'; 40];
51+
// Put a leading zero at the beginning to capture any carry.
52+
buf[0] = b'0';
53+
let len = dtoa::write(&mut buf[1..], value).unwrap() + 1;
54+
let sign = match buf[1] {
55+
s @ b'+' | s @ b'-' => {
56+
buf[1] = b'0';
57+
Some(s)
58+
}
59+
_ => None,
60+
};
61+
// Locate dot, exp, and the first non-zero digit.
62+
let mut pos_dot = None;
63+
let mut pos_exp = None;
64+
let mut prec_start = None;
65+
for i in 1..len {
66+
if buf[i] == b'.' {
67+
debug_assert!(pos_dot.is_none());
68+
pos_dot = Some(i);
69+
} else if buf[i] == b'e' {
70+
pos_exp = Some(i);
71+
break;
72+
} else if prec_start.is_none() && buf[i] != b'0' {
73+
debug_assert!(buf[i] >= b'1' && buf[i] <= b'9');
74+
prec_start = Some(i);
75+
}
76+
}
77+
// If there is no non-zero digit at all, it is just zero.
78+
if prec_start.is_none() {
79+
return dest.write_char('0');
80+
}
81+
let prec_start = prec_start.unwrap();
82+
let coeff_end = pos_exp.unwrap_or(len);
83+
let pos_dot = pos_dot.unwrap_or(coeff_end);
84+
// Find the end position of the number within the given precision.
85+
let prec_end = {
86+
let end = prec_start + PRECISION_FOR_F32;
87+
if pos_dot > prec_start && pos_dot <= end {
88+
end + 1
89+
} else {
90+
end
91+
}
92+
};
93+
let mut new_coeff_end = coeff_end;
94+
if prec_end < coeff_end {
95+
// Round to the given precision.
96+
let next_char = buf[prec_end];
97+
new_coeff_end = prec_end;
98+
if next_char >= b'5' {
99+
for i in (0..prec_end).rev() {
100+
if buf[i] == b'.' {
101+
continue;
102+
}
103+
if buf[i] != b'9' {
104+
buf[i] += 1;
105+
new_coeff_end = i + 1;
106+
break;
107+
}
108+
buf[i] = b'0';
109+
}
110+
}
111+
}
112+
if new_coeff_end < pos_dot {
113+
// If the precision isn't enough to reach the dot, set all digits
114+
// in-between to zero and keep the number until the dot.
115+
for i in new_coeff_end..pos_dot {
116+
buf[i] = b'0';
117+
}
118+
new_coeff_end = pos_dot;
119+
} else {
120+
// Strip any trailing zeros.
121+
for i in (0..new_coeff_end).rev() {
122+
if buf[i] != b'0' {
123+
if buf[i] == b'.' {
124+
new_coeff_end = i;
125+
}
126+
break;
127+
}
128+
new_coeff_end = i;
129+
}
130+
}
131+
// Move exponent part if necessary.
132+
let real_end = if let Some(pos_exp) = pos_exp {
133+
if new_coeff_end != pos_exp {
134+
for i in 0..len - pos_exp {
135+
buf[new_coeff_end + i] = buf[pos_exp + i];
136+
}
137+
}
138+
*has_exp = true;
139+
new_coeff_end + (len - pos_exp)
140+
} else {
141+
new_coeff_end
142+
};
143+
// Add back the sign and strip the leading zero.
144+
let result = if let Some(sign) = sign {
145+
if buf[1] == b'0' && buf[2] != b'.' {
146+
buf[1] = sign;
147+
&buf[1..real_end]
148+
} else {
149+
debug_assert!(buf[0] == b'0');
150+
buf[0] = sign;
151+
&buf[0..real_end]
152+
}
153+
} else {
154+
if buf[1] == b'0' && buf[2] != b'.' {
155+
&buf[2..real_end]
156+
} else {
157+
debug_assert!(buf[0] == b'0');
158+
&buf[1..real_end]
159+
}
160+
};
161+
dest.write_str(str::from_utf8(result).unwrap())
162+
}
44163

45164
#[inline]
46165
fn write_numeric<W>(value: f32, int_value: Option<i32>, has_sign: bool, dest: &mut W)
@@ -50,14 +169,15 @@ fn write_numeric<W>(value: f32, int_value: Option<i32>, has_sign: bool, dest: &m
50169
dest.write_str("+")?;
51170
}
52171

172+
let mut has_exp = false;
53173
if value == 0.0 && value.is_sign_negative() {
54174
// Negative zero. Work around #20596.
55175
dest.write_str("-0")?
56176
} else {
57-
write!(dest, "{}", value)?
177+
dtoa_pretty(dest, value, &mut has_exp)?;
58178
}
59179

60-
if int_value.is_none() && value.fract() == 0. {
180+
if int_value.is_none() && value.fract() == 0. && !has_exp {
61181
dest.write_str(".0")?;
62182
}
63183
Ok(())

src/tests.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ fn almost_equals(a: &Json, b: &Json) -> bool {
3131
(_, &Json::I64(b)) => almost_equals(a, &Json::F64(b as f64)),
3232
(_, &Json::U64(b)) => almost_equals(a, &Json::F64(b as f64)),
3333

34-
(&Json::F64(a), &Json::F64(b)) => (a - b).abs() < 1e-6,
34+
(&Json::F64(a), &Json::F64(b)) => (a - b).abs() <= a.abs() * 1e-6,
3535

3636
(&Json::Boolean(a), &Json::Boolean(b)) => a == b,
3737
(&Json::String(ref a), &Json::String(ref b)) => a == b,
@@ -949,3 +949,23 @@ fn parse_entirely_reports_first_error() {
949949
let result: Result<(), _> = parser.parse_entirely(|_| Err(ParseError::Custom(E::Foo)));
950950
assert_eq!(result, Err(ParseError::Custom(E::Foo)));
951951
}
952+
953+
#[test]
954+
fn roundtrip_numeric_token() {
955+
fn test_roundtrip(value: &str) {
956+
let mut input = ParserInput::new(value);
957+
let mut parser = Parser::new(&mut input);
958+
let token = parser.next().unwrap();
959+
assert_eq!(token.to_css_string(), value);
960+
}
961+
// Test simple number serialization
962+
for i in 0..101 {
963+
test_roundtrip(&format!("{}", i));
964+
for j in 1..10 {
965+
test_roundtrip(&format!("{}.{}", i, j));
966+
for k in 1..10 {
967+
test_roundtrip(&format!("{}.{}{}", i, j, k));
968+
}
969+
}
970+
}
971+
}

0 commit comments

Comments
 (0)