mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Unit fields and length strings read a percentage operand as a share of a value. After * or /, the percentage scales the term before it: 3cm*50% is 1.5 cm and 3cm/50% is 6 cm. After + or -, it adds or takes away that share of everything before it: 50pt+25% is 62.5 pt and 100pt+20pt-50% is 60 pt. Before this change these expressions read as nothing, and the field kept its old value. The parser now splits a sum at its last + or -, so a percentage applies to the whole sum before it. Text without % gives the same values as before, apart from float rounding. A percentage on its own (50%) still reads as nothing, and percent and degree fields still read % as their suffix. Command length strings use the same parser: file.new's width and height, the distance params and the length preferences. Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
132 lines
5.4 KiB
Rust
132 lines
5.4 KiB
Rust
//! Units: format/parse round-trips in every unit, typed units winning over the field's unit,
|
||
//! percentages in arithmetic, names and canvas readouts.
|
||
|
||
use proptest::prelude::*;
|
||
|
||
use super::Unit;
|
||
|
||
#[test]
|
||
fn every_unit_round_trips_through_its_field_text() {
|
||
for u in Unit::ALL {
|
||
// Field text keeps three decimals of the unit: half a step is the most a round trip loses.
|
||
let tol = 0.0005 * u.points() + 1e-9;
|
||
for pt in [0.0, 1.0, 12.5, 72.0, 100.0, 595.28, 1234.5678, -36.0] {
|
||
let shown = u.format(pt);
|
||
let back = u.parse(&shown).unwrap_or_else(|| panic!("{u:?} can't read back `{shown}`"));
|
||
assert!((back - pt).abs() <= tol, "{u:?}: {pt} → `{shown}` → {back}");
|
||
// The bare number reads in the field's unit, and in any unit with its suffix.
|
||
let bare = u.parse(&u.number(pt)).unwrap();
|
||
assert!((bare - pt).abs() <= tol, "{u:?}: bare `{}` → {bare}", u.number(pt));
|
||
let pts = Unit::Points.parse(&shown).unwrap();
|
||
assert!((pts - pt).abs() <= tol, "{u:?}: `{shown}` in a points field → {pts}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn typed_units_win_over_the_fields_unit() {
|
||
let mm = 72.0 / 25.4;
|
||
assert_eq!(Unit::Millimeters.parse("10"), Some(10.0 * mm));
|
||
assert_eq!(Unit::Millimeters.parse("12 pt"), Some(12.0));
|
||
assert_eq!(Unit::Millimeters.parse("1in"), Some(72.0));
|
||
assert_eq!(Unit::Points.parse("5 mm"), Some(5.0 * mm));
|
||
assert_eq!(Unit::Centimeters.parse("2p6"), Some(30.0));
|
||
assert_eq!(Unit::Millimeters.parse("10+5"), Some(15.0 * mm));
|
||
assert_eq!(Unit::Inches.parse("1 + 36 pt"), Some(108.0));
|
||
assert_eq!(Unit::Pixels.parse("-4"), Some(-4.0));
|
||
}
|
||
|
||
#[test]
|
||
fn a_percentage_is_a_share_of_the_value_before_it() {
|
||
let (mm, cm) = (72.0 / 25.4, 72.0 / 2.54);
|
||
for (u, s, want) in [
|
||
(Unit::Centimeters, "3cm*50%", 1.5 * cm),
|
||
(Unit::Points, "50pt+25%", 62.5),
|
||
(Unit::Points, "50pt-25%", 37.5),
|
||
(Unit::Millimeters, "10*50%", 5.0 * mm),
|
||
(Unit::Centimeters, "3cm/50%", 6.0 * cm),
|
||
(Unit::Points, "50 pt + 25 %", 62.5),
|
||
(Unit::Points, "10+20+50%", 45.0),
|
||
// A percentage added or taken away is a share of everything before it. A percentage factor
|
||
// is a share of the term before it.
|
||
(Unit::Points, "100pt+20pt-50%", 60.0),
|
||
(Unit::Points, "100pt-20pt+50%", 120.0),
|
||
(Unit::Points, "100pt+20%-10pt", 110.0),
|
||
(Unit::Points, "10+20*50%", 20.0),
|
||
(Unit::Points, "10*2+50%", 30.0),
|
||
(Unit::Points, "10*2/50%", 40.0),
|
||
(Unit::Points, "10+-50%", 5.0),
|
||
(Unit::Points, "50pt--25%", 62.5),
|
||
(Unit::Centimeters, "3cm*-50%", -1.5 * cm),
|
||
] {
|
||
let got = u.parse(s).unwrap_or_else(|| panic!("{u:?} can't read `{s}`"));
|
||
assert!((got - want).abs() < 1e-9, "{u:?}: `{s}` → {got}, want {want}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn odd_text_reads_as_nothing_without_panicking() {
|
||
for s in ["", "é", "é5", "°+1", "×2", "5 ×", "—3", "mm", "1/0", "--", "+", "50%", "%", "5%%", "%5", "1/0%", "10+%", "10*5pt%"] {
|
||
assert_eq!(Unit::Millimeters.parse(s), None, "{s}");
|
||
}
|
||
}
|
||
|
||
proptest! {
|
||
/// Field text made of digits, operators, `%`, spaces and unit letters reads as a finite value
|
||
/// or as nothing and does not panic.
|
||
#[test]
|
||
fn arithmetic_text_reads_as_a_finite_value_or_nothing(s in "[0-9.+*/% ptxinmcfyd-]{0,24}") {
|
||
for u in Unit::ALL {
|
||
if let Some(v) = u.parse(&s) {
|
||
prop_assert!(v.is_finite(), "{u:?}: `{s}` → {v}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn units_are_named_by_label_key_or_suffix() {
|
||
for u in Unit::ALL {
|
||
assert_eq!(Unit::named(u.label()), Some(u));
|
||
assert_eq!(Unit::named(u.key()), Some(u));
|
||
assert_eq!(Unit::named(&u.label().to_uppercase()), Some(u));
|
||
}
|
||
assert_eq!(Unit::named("mm"), Some(Unit::Millimeters));
|
||
assert_eq!(Unit::named("Feet & Inches"), Some(Unit::FeetInches));
|
||
assert_eq!(Unit::named("feetInches"), Some(Unit::FeetInches));
|
||
assert_eq!(Unit::named("furlongs"), None);
|
||
assert_eq!(Unit::named(""), None);
|
||
}
|
||
|
||
#[test]
|
||
fn readouts_show_two_decimals() {
|
||
assert_eq!(Unit::Points.readout(15.0), "15.00 pt");
|
||
assert_eq!(Unit::Millimeters.readout(72.0), "25.40 mm");
|
||
assert_eq!(Unit::Inches.readout(-36.0), "-0.50 in");
|
||
assert_eq!(Unit::Millimeters.number(1.0), "0.3528");
|
||
// Four decimals for the large units, three for the small ones.
|
||
assert_eq!(Unit::Inches.number(0.5616), "0.0078");
|
||
assert_eq!((Unit::Inches.number(595.2756), Unit::Points.number(595.2756)), ("8.2677".into(), "595.276".into()));
|
||
assert_eq!(Unit::Points.number(-0.00001), "0");
|
||
}
|
||
|
||
#[test]
|
||
fn a_negative_operand_after_an_operator_is_a_sign() {
|
||
assert_eq!(Unit::Points.parse("10*-2"), Some(-20.0));
|
||
assert_eq!(Unit::Points.parse("-10/-2"), Some(5.0));
|
||
assert_eq!(Unit::Points.parse("5--3"), Some(8.0));
|
||
assert_eq!(Unit::Points.parse("5 - -3"), Some(8.0));
|
||
assert_eq!(Unit::Points.parse("5+-3"), Some(2.0));
|
||
assert_eq!(Unit::Points.parse("10-2"), Some(8.0));
|
||
assert_eq!(super::parse_number("90*-1"), Some(-90.0));
|
||
}
|
||
|
||
#[test]
|
||
fn mixed_mul_div() {
|
||
// `*` and `/` share one precedence level and run left to right.
|
||
assert_eq!(Unit::Points.parse("10/2*5"), Some(25.0));
|
||
assert_eq!(crate::parse_number("100*3/4"), Some(75.0));
|
||
assert_eq!(Unit::Points.parse("10*2/5"), Some(4.0));
|
||
assert_eq!(Unit::Points.parse("10*2/5 px"), Some(Unit::Pixels.to_pt(4.0)));
|
||
}
|