Files
vectorcraft/crates/doc/src/tests_units.rs
T
dotCooCoo 7e9d0be723 Fields: unit fields take % in arithmetic, as in 3cm*50% and 50pt+25%
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.
2026-10-16 06:16:35 +07:00

132 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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)));
}