From 3481d8cc81a2b3a14118869c7f0ffe204ff3efc8 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 31 Aug 2021 12:59:53 +0200 Subject: [PATCH] More utility functions - join("a", "b", "c", sep: ", ") - int("12") - float("31.4e-1") - str(10) - sorted((3, 2, 1)) --- src/eval/array.rs | 21 ++++++ src/eval/mod.rs | 3 +- src/eval/ops.rs | 13 +++- src/eval/str.rs | 23 +++++++ src/eval/value.rs | 5 ++ src/library/mod.rs | 13 ++-- src/library/utility.rs | 113 ++++++++++++++++++++++++------- tests/ref/utility/basics.png | Bin 0 -> 873 bytes tests/typ/code/ops-invalid.typ | 4 +- tests/typ/code/ops.typ | 2 + tests/typ/code/repr.typ | 7 +- tests/typ/utility/basics.typ | 69 ++++++++++++++++--- tests/typ/utility/collection.typ | 38 +++++++++++ tests/typ/utility/strings.typ | 8 --- 14 files changed, 264 insertions(+), 55 deletions(-) create mode 100644 tests/ref/utility/basics.png create mode 100644 tests/typ/utility/collection.typ delete mode 100644 tests/typ/utility/strings.typ diff --git a/src/eval/array.rs b/src/eval/array.rs index acf44ab2f..bae89c4b9 100644 --- a/src/eval/array.rs +++ b/src/eval/array.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use std::convert::TryFrom; use std::fmt::{self, Debug, Display, Formatter, Write}; use std::iter::FromIterator; @@ -80,6 +81,26 @@ impl Array { self.0.iter() } + /// Return a sorted version of this array. + /// + /// Returns an error if two values could not be compared. + pub fn sorted(mut self) -> StrResult { + let mut result = Ok(()); + Rc::make_mut(&mut self.0).sort_by(|a, b| { + a.partial_cmp(b).unwrap_or_else(|| { + if result.is_ok() { + result = Err(format!( + "cannot compare {} with {}", + a.type_name(), + b.type_name(), + )); + } + Ordering::Equal + }) + }); + result.map(|_| self) + } + /// Repeat this array `n` times. pub fn repeat(&self, n: i64) -> StrResult { let count = usize::try_from(n) diff --git a/src/eval/mod.rs b/src/eval/mod.rs index b8561a87e..f48312c80 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -5,13 +5,14 @@ mod array; #[macro_use] mod dict; #[macro_use] +mod str; +#[macro_use] mod value; mod capture; mod function; mod ops; mod scope; mod state; -mod str; mod template; mod walk; diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 6d34b8771..c7a456140 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -291,12 +291,21 @@ pub fn compare(lhs: &Value, rhs: &Value) -> Option { match (lhs, rhs) { (Bool(a), Bool(b)) => a.partial_cmp(b), (Int(a), Int(b)) => a.partial_cmp(b), - (Int(a), Float(b)) => (*a as f64).partial_cmp(b), - (Float(a), Int(b)) => a.partial_cmp(&(*b as f64)), (Float(a), Float(b)) => a.partial_cmp(b), (Angle(a), Angle(b)) => a.partial_cmp(b), (Length(a), Length(b)) => a.partial_cmp(b), + (Relative(a), Relative(b)) => a.partial_cmp(b), + (Fractional(a), Fractional(b)) => a.partial_cmp(b), (Str(a), Str(b)) => a.partial_cmp(b), + + // Some technically different things should be comparable. + (&Int(a), &Float(b)) => (a as f64).partial_cmp(&b), + (&Float(a), &Int(b)) => a.partial_cmp(&(b as f64)), + (&Length(a), &Linear(b)) if b.rel.is_zero() => a.partial_cmp(&b.abs), + (&Relative(a), &Linear(b)) if b.abs.is_zero() => a.partial_cmp(&b.rel), + (&Linear(a), &Length(b)) if a.rel.is_zero() => a.abs.partial_cmp(&b), + (&Linear(a), &Relative(b)) if a.abs.is_zero() => a.rel.partial_cmp(&b), + _ => Option::None, } } diff --git a/src/eval/str.rs b/src/eval/str.rs index a358cd9fc..099a43635 100644 --- a/src/eval/str.rs +++ b/src/eval/str.rs @@ -1,3 +1,4 @@ +use std::borrow::Borrow; use std::convert::TryFrom; use std::fmt::{self, Debug, Display, Formatter, Write}; use std::ops::{Add, AddAssign, Deref}; @@ -5,6 +6,16 @@ use std::ops::{Add, AddAssign, Deref}; use crate::diag::StrResult; use crate::util::EcoString; +/// Create a new [`Str`] from a format string. +macro_rules! format_str { + ($($tts:tt)*) => {{ + use std::fmt::Write; + let mut s = $crate::util::EcoString::new(); + write!(s, $($tts)*).unwrap(); + $crate::eval::Str::from(s) + }}; +} + /// A string value with inline storage and clone-on-write semantics. #[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct Str(EcoString); @@ -92,6 +103,18 @@ impl AddAssign for Str { } } +impl AsRef for Str { + fn as_ref(&self) -> &str { + self + } +} + +impl Borrow for Str { + fn borrow(&self) -> &str { + self + } +} + impl From for Str { fn from(c: char) -> Self { Self(c.into()) diff --git a/src/eval/value.rs b/src/eval/value.rs index 5edf0362c..77cb766c2 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -88,6 +88,11 @@ impl Value { { T::cast(self) } + + /// Join the value with another value. + pub fn join(self, rhs: Self) -> StrResult { + ops::join(self, rhs) + } } impl Default for Value { diff --git a/src/library/mod.rs b/src/library/mod.rs index ca99d43ba..102291cc2 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -17,8 +17,8 @@ use std::convert::TryFrom; use std::rc::Rc; use crate::color::{Color, RgbaColor}; -use crate::diag::TypResult; -use crate::eval::{Arguments, EvalContext, Scope, State, Str, Template, Value}; +use crate::diag::{At, TypResult}; +use crate::eval::{Arguments, Array, EvalContext, Scope, State, Str, Template, Value}; use crate::font::{FontFamily, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; use crate::geom::*; use crate::layout::LayoutNode; @@ -59,13 +59,18 @@ pub fn new() -> Scope { // Utility. std.def_func("type", type_); std.def_func("repr", repr); - std.def_func("len", len); - std.def_func("rgb", rgb); + std.def_func("join", join); + std.def_func("int", int); + std.def_func("float", float); + std.def_func("str", str); std.def_func("abs", abs); std.def_func("min", min); std.def_func("max", max); + std.def_func("rgb", rgb); std.def_func("lower", lower); std.def_func("upper", upper); + std.def_func("len", len); + std.def_func("sorted", sorted); // Colors. std.def_const("white", RgbaColor::WHITE); diff --git a/src/library/utility.rs b/src/library/utility.rs index 0ece88acd..19dfc3ec5 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -15,34 +15,65 @@ pub fn repr(_: &mut EvalContext, args: &mut Arguments) -> TypResult { Ok(args.expect::("value")?.to_string().into()) } -/// `len`: The length of a string, an array or a dictionary. -pub fn len(_: &mut EvalContext, args: &mut Arguments) -> TypResult { - let Spanned { v, span } = args.expect("collection")?; +/// `join`: Join a sequence of values, optionally interspersing it with another +/// value. +pub fn join(_: &mut EvalContext, args: &mut Arguments) -> TypResult { + let span = args.span; + let sep = args.named::("sep")?.unwrap_or(Value::None); + + let mut result = Value::None; + let mut iter = args.all::(); + + if let Some(first) = iter.next() { + result = first; + } + + for value in iter { + result = result.join(sep.clone()).at(span)?; + result = result.join(value).at(span)?; + } + + Ok(result) +} + +/// `int`: Try to convert a value to a integer. +pub fn int(_: &mut EvalContext, args: &mut Arguments) -> TypResult { + let Spanned { v, span } = args.expect("value")?; Ok(Value::Int(match v { - Value::Str(v) => v.len(), - Value::Array(v) => v.len(), - Value::Dict(v) => v.len(), - _ => bail!(span, "expected string, array or dictionary"), + Value::Bool(v) => v as i64, + Value::Int(v) => v, + Value::Float(v) => v as i64, + Value::Str(v) => match v.parse() { + Ok(v) => v, + Err(_) => bail!(span, "invalid integer"), + }, + v => bail!(span, "cannot convert {} to integer", v.type_name()), })) } -/// `rgb`: Create an RGB(A) color. -pub fn rgb(_: &mut EvalContext, args: &mut Arguments) -> TypResult { - Ok(Value::Color(Color::Rgba( - if let Some(string) = args.eat::>() { - match RgbaColor::from_str(&string.v) { - Ok(color) => color, - Err(_) => bail!(string.span, "invalid color"), - } - } else { - let r = args.expect("red component")?; - let g = args.expect("green component")?; - let b = args.expect("blue component")?; - let a = args.eat().unwrap_or(1.0); - let f = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8; - RgbaColor::new(f(r), f(g), f(b), f(a)) +/// `float`: Try to convert a value to a float. +pub fn float(_: &mut EvalContext, args: &mut Arguments) -> TypResult { + let Spanned { v, span } = args.expect("value")?; + Ok(Value::Float(match v { + Value::Int(v) => v as f64, + Value::Float(v) => v, + Value::Str(v) => match v.parse() { + Ok(v) => v, + Err(_) => bail!(span, "invalid float"), }, - ))) + v => bail!(span, "cannot convert {} to float", v.type_name()), + })) +} + +/// `str`: Try to convert a value to a string. +pub fn str(_: &mut EvalContext, args: &mut Arguments) -> TypResult { + let Spanned { v, span } = args.expect("value")?; + Ok(Value::Str(match v { + Value::Int(v) => format_str!("{}", v), + Value::Float(v) => format_str!("{}", v), + Value::Str(v) => v, + v => bail!(span, "cannot convert {} to string", v.type_name()), + })) } /// `abs`: The absolute value of a numeric value. @@ -91,6 +122,25 @@ fn minmax(args: &mut Arguments, goal: Ordering) -> TypResult { Ok(extremum) } +/// `rgb`: Create an RGB(A) color. +pub fn rgb(_: &mut EvalContext, args: &mut Arguments) -> TypResult { + Ok(Value::Color(Color::Rgba( + if let Some(string) = args.eat::>() { + match RgbaColor::from_str(&string.v) { + Ok(color) => color, + Err(_) => bail!(string.span, "invalid color"), + } + } else { + let r = args.expect("red component")?; + let g = args.expect("green component")?; + let b = args.expect("blue component")?; + let a = args.eat().unwrap_or(1.0); + let f = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8; + RgbaColor::new(f(r), f(g), f(b), f(a)) + }, + ))) +} + /// `lower`: Convert a string to lowercase. pub fn lower(_: &mut EvalContext, args: &mut Arguments) -> TypResult { Ok(args.expect::("string")?.to_lowercase().into()) @@ -100,3 +150,20 @@ pub fn lower(_: &mut EvalContext, args: &mut Arguments) -> TypResult { pub fn upper(_: &mut EvalContext, args: &mut Arguments) -> TypResult { Ok(args.expect::("string")?.to_uppercase().into()) } + +/// `len`: The length of a string, an array or a dictionary. +pub fn len(_: &mut EvalContext, args: &mut Arguments) -> TypResult { + let Spanned { v, span } = args.expect("collection")?; + Ok(Value::Int(match v { + Value::Str(v) => v.len(), + Value::Array(v) => v.len(), + Value::Dict(v) => v.len(), + _ => bail!(span, "expected string, array or dictionary"), + })) +} + +/// `sorted`: The sorted version of an array. +pub fn sorted(_: &mut EvalContext, args: &mut Arguments) -> TypResult { + let Spanned { v, span } = args.expect::>("array")?; + Ok(Value::Array(v.sorted().at(span)?)) +} diff --git a/tests/ref/utility/basics.png b/tests/ref/utility/basics.png new file mode 100644 index 0000000000000000000000000000000000000000..fd276046273e30bf2eb911aa9295ad6526106f52 GIT binary patch literal 873 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZy*PjbLxo#u9RmZifv1aONX4zUH)1C~wi0N2 zn3iDdA#sBzg^iE7z45R^+JnRgi3<)dXm3>DaB{9V-@16!+_h&@f=su1&b@r!)k{40 zXx*3RE=L}Jtgw;mKmPb<+K>511!WFmNB^`r94bRu4%pe*?Yo+9cU|m5-uF|c*Tg=g zoi3~kW%*LVk*xl_`Y=n*B|g*d%ftkpMy#&R%$f9K6^Hj?hMkk&#%=GBp0M<`{*G|p zKBeBWaozh*AJbbj3YU7k zd1Z1-U;pQBj@w^#uRi$_!m@h(>7Da`^h{Wyzrag-|GH~RUk?}vHY-R6%v#J?yx_!g z@ed*K$Lrh~)?|w2q)y^4Xgq!LpBYobS))C*d*<^?J6N9HnIJA{2;-An180;e(MDE+;VvS#w*f;$d% zS&25kf^Th;c>Hzu=2dB3tF#sr-czp(&OT|lGOtix|E_;awt05X?|;*@n4YXyb(-wy~P+bPqbE{+DJ2r0lB-2ve_~pjKt$8-q_Pq>Ja#1VZf7X`xW6)%0wqf?Zmd5`V pUvcz3z=G_XA3ziL$3KeynM~&$nOk@)*ASG4JYD@<);T3K0RRf`i>Ckp literal 0 HcmV?d00001 diff --git a/tests/typ/code/ops-invalid.typ b/tests/typ/code/ops-invalid.typ index 5d371e919..1355181af 100644 --- a/tests/typ/code/ops-invalid.typ +++ b/tests/typ/code/ops-invalid.typ @@ -26,8 +26,8 @@ {not ()} --- -// Error: 2-12 cannot apply '<=' to relative and relative -{30% <= 40%} +// Error: 2-18 cannot apply '<=' to linear and relative +{30% + 1pt <= 40%} --- // Special messages for +, -, * and /. diff --git a/tests/typ/code/ops.typ b/tests/typ/code/ops.typ index 201f86da6..149837b29 100644 --- a/tests/typ/code/ops.typ +++ b/tests/typ/code/ops.typ @@ -146,6 +146,8 @@ #test(5 <= 5, true) #test(5 <= 4, false) #test(45deg < 1rad, true) +#test(10% < 20%, true) +#test(50% < 40% + 0pt, false) --- // Test assignment operators. diff --git a/tests/typ/code/repr.typ b/tests/typ/code/repr.typ index 35a47e497..22890ff10 100644 --- a/tests/typ/code/repr.typ +++ b/tests/typ/code/repr.typ @@ -51,10 +51,5 @@ {() => none} --- -// Test using the `repr` function. - -// Returns a string. -#test(repr((1, 2, false, )), "(1, 2, false)") - -// Not in monospace +// When using the `repr` function it's not in monospace. #repr(23deg) diff --git a/tests/typ/utility/basics.typ b/tests/typ/utility/basics.typ index 203b7eb1a..a2f6b220a 100644 --- a/tests/typ/utility/basics.typ +++ b/tests/typ/utility/basics.typ @@ -2,16 +2,67 @@ // Ref: false --- -// Test the `len` function. -#test(len(()), 0) -#test(len(("A", "B", "C")), 3) -#test(len("Hello World!"), 12) -#test(len((a: 1, b: 2)), 2) +// Test the `type` function. +#test(type(1), "integer") +#test(type(ltr), "direction") --- -// Error: 5-7 missing argument: collection -#len() +// Test the `repr` function. +#test(repr(ltr), "ltr") +#test(repr((1, 2, false, )), "(1, 2, false)") --- -// Error: 6-10 expected string, array or dictionary -#len(12pt) +// Test the `join` function. +#test(join(), none) +#test(join(sep: false), none) +#test(join(1), 1) +#test(join("a", "b", "c"), "abc") +#test("(" + join("a", "b", "c", sep: ", ") + ")", "(a, b, c)") + +--- +// Test joining templates. +// Ref: true +#join([One], [Two], [Three], sep: [, ]). + +--- +// Error: 11-24 cannot join boolean with boolean +#test(join(true, false)) + +--- +// Error: 11-29 cannot join string with integer +#test(join("a", "b", sep: 1)) + +--- +// Test conversion functions. +#test(int(false), 0) +#test(int(true), 1) +#test(int(10), 10) +#test(int("150"), 150) +#test(type(10 / 3), "float") +#test(int(10 / 3), 3) +#test(float(10), 10.0) +#test(float("31.4e-1"), 3.14) +#test(type(float(10)), "float") +#test(str(123), "123") +#test(str(50.14), "50.14") +#test(len(str(10 / 3)) > 10, true) + +--- +// Error: 6-10 cannot convert length to integer +#int(10pt) + +--- +// Error: 8-13 cannot convert function to float +#float(float) + +--- +// Error: 6-8 cannot convert template to string +#str([]) + +--- +// Error: 6-12 invalid integer +#int("nope") + +--- +// Error: 8-15 invalid float +#float("1.2.3") diff --git a/tests/typ/utility/collection.typ b/tests/typ/utility/collection.typ new file mode 100644 index 000000000..a97184d35 --- /dev/null +++ b/tests/typ/utility/collection.typ @@ -0,0 +1,38 @@ +// Test collection functions. +// Ref: false + +--- +#let memes = "ArE mEmEs gReAt?"; +#test(lower(memes), "are memes great?") +#test(upper(memes), "ARE MEMES GREAT?") +#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ") + +--- +// Test the `len` function. +#test(len(()), 0) +#test(len(("A", "B", "C")), 3) +#test(len("Hello World!"), 12) +#test(len((a: 1, b: 2)), 2) + +--- +// Error: 5-7 missing argument: collection +#len() + +--- +// Error: 6-10 expected string, array or dictionary +#len(12pt) + +--- +// Test the `sorted` function. +#test(sorted(()), ()) +#test(sorted((true, false) * 10), (false,) * 10 + (true,) * 10) +#test(sorted(("it", "the", "hi", "text")), ("hi", "it", "text", "the")) +#test(sorted((2, 1, 3, 10, 5, 8, 6, -7, 2)), (-7, 1, 2, 2, 3, 5, 6, 8, 10)) + +--- +// Error: 9-21 cannot compare string with integer +#sorted((1, 2, "ab")) + +--- +// Error: 9-24 cannot compare template with template +#sorted(([Hi], [There])) diff --git a/tests/typ/utility/strings.typ b/tests/typ/utility/strings.typ deleted file mode 100644 index 7c708175c..000000000 --- a/tests/typ/utility/strings.typ +++ /dev/null @@ -1,8 +0,0 @@ -// Test string functions. -// Ref: false - ---- -#let memes = "ArE mEmEs gReAt?"; -#test(lower(memes), "are memes great?") -#test(upper(memes), "ARE MEMES GREAT?") -#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ")