From 7e07b61046ec43369fb8f2e281b71c4e5988f5fc Mon Sep 17 00:00:00 2001 From: Lynn Date: Tue, 30 May 2023 18:05:15 +0200 Subject: [PATCH] Add a base parameter to str() (#1362) --- library/src/compute/construct.rs | 103 ++++++++++++++++++++++++++++--- tests/typ/compute/construct.typ | 11 ++++ 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/library/src/compute/construct.rs b/library/src/compute/construct.rs index 4ff9040b0..169c53284 100644 --- a/library/src/compute/construct.rs +++ b/library/src/compute/construct.rs @@ -456,13 +456,15 @@ cast_from_value! { /// Convert a value to a string. /// -/// - Integers are formatted in base 10. +/// - Integers are formatted in base 10. This can be overridden with the +/// optional `base` parameter. /// - Floats are formatted in base 10 and never in exponential notation. /// - From labels the name is extracted. /// /// ## Example { #example } /// ```example /// #str(10) \ +/// #str(4000, base: 16) \ /// #str(2.7) \ /// #str(1e8) \ /// #str() @@ -475,19 +477,80 @@ cast_from_value! { pub fn str( /// The value that should be converted to a string. value: ToStr, + /// The base (radix) to display integers in, between 2 and 36. + #[named] + #[default(Spanned::new(10, Span::detached()))] + base: Spanned, ) -> Value { - Value::Str(value.0) + match value { + ToStr::Str(s) => { + if base.v != 10 { + bail!(base.span, "base is only supported for integers"); + } + Value::Str(s) + } + ToStr::Int(n) => { + if base.v < 2 || base.v > 36 { + bail!(base.span, "base must be between 2 and 36"); + } + int_to_base(n, base.v).into() + } + } } /// A value that can be cast to a string. -struct ToStr(Str); +enum ToStr { + /// A string value ready to be used as-is. + Str(Str), + /// An integer about to be formatted in a given base. + Int(i64), +} cast_from_value! { ToStr, - v: i64 => Self(format_str!("{}", v)), - v: f64 => Self(format_str!("{}", v)), - v: Label => Self(v.0.into()), - v: Str => Self(v), + v: i64 => Self::Int(v), + v: f64 => Self::Str(format_str!("{}", v)), + v: Label => Self::Str(v.0.into()), + v: Str => Self::Str(v), +} + +/// Format an integer in a base. +fn int_to_base(mut n: i64, base: i64) -> EcoString { + if n == 0 { + return "0".into(); + } + + // In Rust, `format!("{:x}", -14i64)` is not `-e` but `fffffffffffffff2`. + // So we can only use the built-in for decimal, not bin/oct/hex. + if base == 10 { + return eco_format!("{n}"); + } + + // The largest output is `to_base(i64::MIN, 2)`, which is 65 chars long. + const SIZE: usize = 65; + let mut digits = [b'\0'; SIZE]; + let mut i = SIZE; + + // It's tempting to take the absolute value, but this will fail for i64::MIN. + // Instead, we turn n negative, as -i64::MAX is perfectly representable. + let negative = n < 0; + if n > 0 { + n = -n; + } + + while n != 0 { + let digit = char::from_digit(-(n % base) as u32, base as u32); + i -= 1; + digits[i] = digit.unwrap_or('?') as u8; + n /= base; + } + + if negative { + i -= 1; + digits[i] = b'-'; + } + + std::str::from_utf8(&digits[i..]).unwrap_or_default().into() } /// Create a label from a string. @@ -611,3 +674,29 @@ pub fn range( Value::Array(array) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_base() { + assert_eq!(&int_to_base(0, 10), "0"); + assert_eq!(&int_to_base(0, 16), "0"); + assert_eq!(&int_to_base(0, 36), "0"); + assert_eq!( + &int_to_base(i64::MAX, 2), + "111111111111111111111111111111111111111111111111111111111111111" + ); + assert_eq!( + &int_to_base(i64::MIN, 2), + "-1000000000000000000000000000000000000000000000000000000000000000" + ); + assert_eq!(&int_to_base(i64::MAX, 10), "9223372036854775807"); + assert_eq!(&int_to_base(i64::MIN, 10), "-9223372036854775808"); + assert_eq!(&int_to_base(i64::MAX, 16), "7fffffffffffffff"); + assert_eq!(&int_to_base(i64::MIN, 16), "-8000000000000000"); + assert_eq!(&int_to_base(i64::MAX, 36), "1y2p0ij32e8e7"); + assert_eq!(&int_to_base(i64::MIN, 36), "-1y2p0ij32e8e8"); + } +} diff --git a/tests/typ/compute/construct.typ b/tests/typ/compute/construct.typ index 9c05a6d07..aea15b53e 100644 --- a/tests/typ/compute/construct.typ +++ b/tests/typ/compute/construct.typ @@ -64,6 +64,9 @@ --- // Test conversion to string. #test(str(123), "123") +#test(str(123, base: 3), "11120") +#test(str(-123, base: 16), "-7b") +#test(str(9223372036854775807, base: 36), "1y2p0ij32e8e7") #test(str(50.14), "50.14") #test(str(10 / 3).len() > 10, true) @@ -71,6 +74,14 @@ // Error: 6-8 expected integer, float, label, or string, found content #str([]) +--- +// Error: 17-19 base must be between 2 and 36 +#str(123, base: 99) + +--- +// Error: 18-19 base is only supported for integers +#str(1.23, base: 2) + --- #assert(range(2, 5) == (2, 3, 4))