From 11445bb35f5258c6a0c09efe9d0ed6e4ee7b5e4b Mon Sep 17 00:00:00 2001 From: Eivind Fonn Date: Thu, 30 Nov 2023 14:41:27 +0100 Subject: [PATCH] Implement string formatting specs --- gold/src/ast.rs | 137 +++++++-- gold/src/error.rs | 6 +- gold/src/eval.rs | 4 +- gold/src/object.rs | 318 +++++++++++++++++++- gold/src/parsing.rs | 63 ++-- gold/src/tests/object.rs | 611 +++++++++++++++++++++++++++++++++++++- gold/src/tests/parsing.rs | 8 +- 7 files changed, 1075 insertions(+), 72 deletions(-) diff --git a/gold/src/ast.rs b/gold/src/ast.rs index 77454ad..7c7f367 100644 --- a/gold/src/ast.rs +++ b/gold/src/ast.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use serde::{Deserialize, Serialize}; use crate::error::{BindingType, Span, Syntax}; +use crate::object::{StringFormatSpec, IntegerFormatSpec, FloatFormatSpec}; use super::error::{Error, Tagged, Action}; use super::object::{Object, Key}; @@ -244,62 +245,165 @@ impl Validatable for Binding { // StringElement // ---------------------------------------------------------------- -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum AlignSpec { +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum StringAlignSpec { Left, Right, - AfterSign, Center, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum AlignSpec { + AfterSign, + String(StringAlignSpec), +} + +impl Default for AlignSpec { + fn default() -> Self { + Self::left() + } +} + +impl AlignSpec { + pub fn left() -> Self { + Self::String(StringAlignSpec::Left) + } + + pub fn right() -> Self { + Self::String(StringAlignSpec::Right) + } + + pub fn center() -> Self { + Self::String(StringAlignSpec::Center) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum SignSpec { Plus, Minus, Space, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +impl Default for SignSpec { + fn default() -> Self { + Self::Minus + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum GroupingSpec { Comma, Underscore, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum UppercaseSpec { Upper, Lower, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum FormatType { - String, - +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum IntegerFormatType { Binary, Character, Decimal, Octal, Hex(UppercaseSpec), +} +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum FloatFormatType { Sci(UppercaseSpec), - Fixed(UppercaseSpec), - General(UppercaseSpec), + Fixed, + General, Percentage, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum FormatType { + String, + Integer(IntegerFormatType), + Float(FloatFormatType), +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct FormatSpec { pub fill: char, pub align: Option, pub sign: Option, pub alternate: bool, - pub zero: bool, - pub width: Option, + pub width: Option, pub grouping: Option, - pub precision: Option, + pub precision: Option, pub fmt_type: Option, } +impl FormatSpec { + pub(crate) fn string_spec(&self) -> Option { + if self.sign.is_some() || + self.alternate || + self.grouping.is_some() || + self.precision.is_some() || + self.fmt_type.is_some_and(|x| x != FormatType::String) + { + return None; + } + + let align = match self.align { + Some(AlignSpec::AfterSign) => { return None; }, + Some(AlignSpec::String(x)) => x, + None => StringAlignSpec::Left, + }; + + Some(StringFormatSpec { + fill: self.fill, + width: self.width, + align, + }) + } + + pub(crate) fn integer_spec(&self) -> Option { + if self.precision.is_some() { + return None; + } + + let fmt_type = match self.fmt_type { + None => IntegerFormatType::Decimal, + Some(FormatType::Integer(x)) => x, + _ => { return None; }, + }; + + Some(IntegerFormatSpec { + fill: self.fill, + align: self.align.unwrap_or_else(AlignSpec::right), + sign: self.sign.unwrap_or_default(), + alternate: self.alternate, + width: self.width, + grouping: self.grouping, + fmt_type, + }) + } + + pub(crate) fn float_spec(&self) -> Option { + let fmt_type = match self.fmt_type { + Some(FormatType::Float(x)) => x, + None => if self.precision.is_some() { FloatFormatType::Fixed } else { FloatFormatType::General }, + _ => { return None; }, + }; + + Some(FloatFormatSpec { + fill: self.fill, + align: self.align.unwrap_or_else(AlignSpec::right), + sign: self.sign.unwrap_or_default(), + width: self.width, + grouping: self.grouping, + precision: self.precision.unwrap_or(6), + fmt_type, + }) + } +} + impl Default for FormatSpec{ fn default() -> Self { Self { @@ -307,7 +411,6 @@ impl Default for FormatSpec{ align: None, sign: None, alternate: false, - zero: false, width: None, grouping: None, precision: None, diff --git a/gold/src/error.rs b/gold/src/error.rs index a119057..b49ee0b 100644 --- a/gold/src/error.rs +++ b/gold/src/error.rs @@ -517,6 +517,9 @@ pub enum TypeMismatch { /// Attempted to string interpolate an exotic type. Interpolate(Type), + /// Attempted to interpolate a type with the wrong format spec. + InterpolateSpec(Type), + /// Two types were incompatible with a binary operator. BinOp(Type, Type, BinOp), @@ -854,7 +857,8 @@ impl Display for Reason { Self::TypeMismatch(TypeMismatch::Call(x)) => f.write_fmt(format_args!("unsuitable type for function call: {}", x)), Self::TypeMismatch(TypeMismatch::ExpectedPosArg { index, allowed, received }) => fmt_expected_arg(f, index + 1, allowed, received), Self::TypeMismatch(TypeMismatch::ExpectedKwArg { name, allowed, received }) => fmt_expected_arg(f, name, allowed, received), - Self::TypeMismatch(TypeMismatch::Interpolate(x)) => f.write_fmt(format_args!("unsuitable type for string interpolation: {}", x)), + Self::TypeMismatch(TypeMismatch::Interpolate(x)) => f.write_fmt(format_args!("unsuitable type for interpolation: {}", x)), + Self::TypeMismatch(TypeMismatch::InterpolateSpec(x)) => f.write_fmt(format_args!("unsuitable type for format spec: {}", x)), Self::TypeMismatch(TypeMismatch::Iterate(x)) => f.write_fmt(format_args!("non-iterable type: {}", x)), Self::TypeMismatch(TypeMismatch::Json(x)) => f.write_fmt(format_args!("unsuitable type for JSON-like conversion: {}", x)), Self::TypeMismatch(TypeMismatch::MapKey(x)) => f.write_fmt(format_args!("unsuitable type for map key: {}", x)), diff --git a/gold/src/eval.rs b/gold/src/eval.rs index b72bf10..36180d2 100644 --- a/gold/src/eval.rs +++ b/gold/src/eval.rs @@ -484,9 +484,9 @@ impl<'a> Namespace<'a> { for element in elements { match element { StringElement::Raw(val) => rval += val.as_ref(), - StringElement::Interpolate(expr, _) => { + StringElement::Interpolate(expr, spec) => { let val = self.eval(expr)?; - let text = val.format().map_err(expr.tag_error(Action::Format))?; + let text = val.format(spec.clone().unwrap_or_default()).map_err(expr.tag_error(Action::Format))?; rval += &text; } } diff --git a/gold/src/object.rs b/gold/src/object.rs index 22bd8e1..256c33b 100644 --- a/gold/src/object.rs +++ b/gold/src/object.rs @@ -33,7 +33,7 @@ use symbol_table::GlobalSymbol; use crate::builtins::BUILTINS; use crate::traits::{ToVec, ToMap}; -use crate::ast::{ListBinding, MapBinding, Expr, BinOp, UnOp}; +use crate::ast::{ListBinding, MapBinding, Expr, BinOp, UnOp, FormatSpec, StringAlignSpec, AlignSpec, IntegerFormatType, GroupingSpec, SignSpec, UppercaseSpec, FloatFormatType}; use crate::error::{Error, Tagged, TypeMismatch, Value, Reason}; use crate::eval::Namespace; use crate::util; @@ -99,6 +99,150 @@ impl Display for Type { } + +// Formatting +// ------------------------------------------------------------------------------------------------ + +pub(crate) struct StringFormatSpec { + pub fill: char, + pub align: StringAlignSpec, + pub width: Option, +} + +fn fmt_str(s: &str, spec: StringFormatSpec) -> String { + match spec.width { + None => s.to_owned(), + Some(w) => { + let nchars = s.chars().count(); + if nchars > w { + s.to_owned() + } else { + let missing = w - nchars; + let (lfill, rfill) = match spec.align { + StringAlignSpec::Left => (0, missing), + StringAlignSpec::Right => (missing, 0), + StringAlignSpec::Center => (missing / 2, missing - missing / 2), + }; + let mut r = String::with_capacity(w); + for _ in 0..lfill { + r.push(spec.fill); + } + r += s; + for _ in 0..rfill { + r.push(spec.fill); + } + r + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub(crate) struct FloatFormatSpec { + pub fill: char, + pub align: AlignSpec, + pub sign: SignSpec, + pub width: Option, + pub grouping: Option, + pub precision: usize, + pub fmt_type: FloatFormatType, +} + +impl FloatFormatSpec { + fn string_spec(&self) -> Option { + match (self.align, self.width) { + (AlignSpec::AfterSign, _) => { return None; }, + (_, None) => { return None; } + (AlignSpec::String(align), Some(width)) => Some( + StringFormatSpec { fill: self.fill, align, width: Some(width) } + ), + } + } +} + +fn fmt_float(f: f64, spec: FloatFormatSpec) -> String { + let base = match spec.fmt_type { + FloatFormatType::Fixed => format!("{number:+.precision$}", number = f, precision = spec.precision), + FloatFormatType::General => format!("{:+}", f), + FloatFormatType::Sci(UppercaseSpec::Lower) => + format!("{number:+.precision$e}", number = f, precision = spec.precision), + FloatFormatType::Sci(UppercaseSpec::Upper) => + format!("{number:+.precision$E}", number = f, precision = spec.precision), + FloatFormatType::Percentage => + format!("{number:+.precision$}%", number = 100.0*f, precision = spec.precision), + }; + + let mut base_digits = &base[1..]; + let mut buffer = String::new(); + + match (spec.sign, f < 0.0) { + (_, true) => { buffer.push('-'); }, + (SignSpec::Plus, false) => { buffer.push('+'); } + (SignSpec::Space, false) => { buffer.push(' '); } + _ => {}, + } + + if let Some(group_spec) = spec.grouping { + let group_size: usize = 3; + let group_char = match group_spec { + GroupingSpec::Comma => ',', + GroupingSpec::Underscore => '_', + }; + + let mut num_digits: usize = base_digits.len(); + for (i, c) in base_digits.char_indices() { + match c { + '0'..='9' => {}, + _ => { num_digits = i; break; } + } + } + + let num_groups = (num_digits + group_size - 1) / group_size; + let first_group_size = num_digits - (num_groups - 1) * group_size; + + if let (AlignSpec::AfterSign, Some(min_width)) = (spec.align, spec.width) { + let num_width = base_digits.len() + buffer.len() + num_groups - 1; + if num_width < min_width { + let extra = min_width - num_width; + for _ in 0..extra { + buffer.push(spec.fill); + } + } + } + + buffer += &base_digits[..first_group_size]; + base_digits = &base_digits[first_group_size..]; + + for _ in 1..num_groups { + buffer.push(group_char); + buffer += &base_digits[..group_size]; + base_digits = &base_digits[group_size..]; + } + + buffer += base_digits; + } else { + if let (AlignSpec::AfterSign, Some(min_width)) = (spec.align, spec.width) { + let num_width = base_digits.len() + buffer.len(); + if num_width < min_width { + let extra = min_width - num_width; + for _ in 0..extra { + buffer.push(spec.fill); + } + } + } + + buffer += base_digits; + } + + if let Some(str_spec) = spec.string_spec() { + fmt_str(buffer.as_str(), str_spec) + } else { + buffer + } +} + + + // String variant // ------------------------------------------------------------------------------------------------ @@ -196,11 +340,31 @@ impl StrVariant { // Integer variant // ------------------------------------------------------------------------------------------------ +pub(crate) struct IntegerFormatSpec { + pub fill: char, + pub align: AlignSpec, + pub sign: SignSpec, + pub alternate: bool, + pub width: Option, + pub grouping: Option, + pub fmt_type: IntegerFormatType, +} + +impl IntegerFormatSpec { + fn string_spec(&self) -> Option { + match (self.align, self.width) { + (AlignSpec::AfterSign, _) => { return None; }, + (_, None) => { return None; } + (AlignSpec::String(align), Some(width)) => Some( + StringFormatSpec { fill: self.fill, align, width: Some(width) } + ), + } + } +} /// The integer variant represents all possible Gold integers. #[derive(Clone, Serialize, Deserialize, PartialEq, Debug)] pub enum IntVariant { - /// Machine integers. Small(i64), @@ -514,6 +678,98 @@ impl IntVariant { (Self::Big(x), Self::Big(y)) => x.eq(y), } } + + fn format(&self, spec: IntegerFormatSpec) -> Result { + let base = match (spec.fmt_type, self) { + (IntegerFormatType::Character, _) => { + let codepoint = u32::try_from(self).map_err(|_| Error::new(Value::OutOfRange))?; + let c = char::try_from(codepoint).map_err(|_| Error::new(Value::OutOfRange))?; + return Ok(c.to_string()); + }, + (IntegerFormatType::Binary, Self::Small(x)) => format!("{:+b}", x), + (IntegerFormatType::Binary, Self::Big(x)) => format!("{:+b}", x.as_ref()), + (IntegerFormatType::Decimal, Self::Small(x)) => format!("{:+}", x), + (IntegerFormatType::Decimal, Self::Big(x)) => format!("{:+}", x), + (IntegerFormatType::Octal, Self::Small(x)) => format!("{:+o}", x), + (IntegerFormatType::Octal, Self::Big(x)) => format!("{:+o}", x.as_ref()), + (IntegerFormatType::Hex(UppercaseSpec::Lower), Self::Small(x)) => format!("{:+x}", x), + (IntegerFormatType::Hex(UppercaseSpec::Lower), Self::Big(x)) => format!("{:+x}", x.as_ref()), + (IntegerFormatType::Hex(UppercaseSpec::Upper), Self::Small(x)) => format!("{:+X}", x), + (IntegerFormatType::Hex(UppercaseSpec::Upper), Self::Big(x)) => format!("{:+X}", x.as_ref()), + }; + + let mut base_digits = &base[1..]; + let mut buffer = String::new(); + + match (spec.sign, self < &Self::Small(0)) { + (_, true) => { buffer.push('-'); }, + (SignSpec::Plus, false) => { buffer.push('+'); } + (SignSpec::Space, false) => { buffer.push(' '); } + _ => {}, + } + + if spec.alternate { + match spec.fmt_type { + IntegerFormatType::Binary => { buffer += "0b"; }, + IntegerFormatType::Octal => { buffer += "0o"; }, + IntegerFormatType::Hex(UppercaseSpec::Lower) => { buffer += "0x"; }, + IntegerFormatType::Hex(UppercaseSpec::Upper) => { buffer += "0X"; }, + _ => {}, + } + } + + if let Some(group_spec) = spec.grouping { + let group_size: usize = match spec.fmt_type { + IntegerFormatType::Decimal => 3, + _ => 4, + }; + + let group_char = match group_spec { + GroupingSpec::Comma => ',', + GroupingSpec::Underscore => '_', + }; + + let num_groups = (base_digits.len() + group_size - 1) / group_size; + let first_group_size = base_digits.len() - (num_groups - 1) * group_size; + + if let (AlignSpec::AfterSign, Some(min_width)) = (spec.align, spec.width) { + let num_width = base_digits.len() + buffer.len() + num_groups - 1; + if num_width < min_width { + let extra = min_width - num_width; + for _ in 0..extra { + buffer.push(spec.fill); + } + } + } + + buffer += &base_digits[..first_group_size]; + base_digits = &base_digits[first_group_size..]; + + for _ in 1..num_groups { + buffer.push(group_char); + buffer += &base_digits[..group_size]; + base_digits = &base_digits[group_size..]; + } + } else { + if let (AlignSpec::AfterSign, Some(min_width)) = (spec.align, spec.width) { + let num_width = base_digits.len() + buffer.len(); + if num_width < min_width { + let extra = min_width - num_width; + for _ in 0..extra { + buffer.push(spec.fill); + } + } + } + + buffer += base_digits; + } + + if let Some(str_spec) = spec.string_spec() { + Ok(fmt_str(buffer.as_str(), str_spec)) + } else { + Ok(buffer) + } + } } @@ -527,7 +783,6 @@ impl IntVariant { /// looked up in the [`BUILTINS`] mapping. #[derive(Clone)] pub struct Builtin { - /// The rust callable for evaluating the function. pub func: fn(&List, Option<&Map>) -> Result, @@ -687,7 +942,6 @@ impl FuncVariant { /// struct enclosing an `ObjectVariant`. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum ObjectVariant { - /// Integers Int(IntVariant), @@ -743,7 +997,6 @@ impl PartialOrd for ObjectVariant { } impl ObjectVariant { - /// Convert back into an object. pub fn object(self) -> Object { Object(self) @@ -789,14 +1042,52 @@ impl ObjectVariant { } /// String representation of this object. Used for string interpolation. - pub fn format(&self) -> Result { + pub fn format(&self, spec: FormatSpec) -> Result { match self { - Self::Str(r) => Ok(r.as_str().to_owned()), - Self::Int(r) => Ok(r.to_string()), - Self::Float(r) => Ok(r.to_string()), - Self::Boolean(true) => Ok("true".to_string()), - Self::Boolean(false) => Ok("false".to_string()), - Self::Null => Ok("null".to_string()), + Self::Str(r) => { + if let Some(str_spec) = spec.string_spec() { + Ok(fmt_str(r.as_str(), str_spec)) + } else { + Err(Error::new(TypeMismatch::InterpolateSpec(self.type_of()))) + } + }, + Self::Boolean(x) => { + if let Some(str_spec) = spec.string_spec() { + let s = if *x { "true" } else { "false" }; + Ok(fmt_str(s, str_spec)) + } else if let Some(int_spec) = spec.integer_spec() { + let i = if *x { 1 } else { 0 }; + IntVariant::Small(i).format(int_spec) + } else if let Some(float_spec) = spec.float_spec() { + let f = if *x { 1.0 } else { 0.0 }; + Ok(fmt_float(f, float_spec)) + } else { + Err(Error::new(TypeMismatch::InterpolateSpec(self.type_of()))) + } + }, + Self::Null => { + if let Some(str_spec) = spec.string_spec() { + Ok(fmt_str("null", str_spec)) + } else { + Err(Error::new(TypeMismatch::InterpolateSpec(self.type_of()))) + } + }, + Self::Int(r) => { + if let Some(int_spec) = spec.integer_spec() { + r.format(int_spec) + } else if let Some(float_spec) = spec.float_spec() { + Ok(fmt_float(r.to_f64(), float_spec)) + } else { + Err(Error::new(TypeMismatch::InterpolateSpec(self.type_of()))) + } + }, + Self::Float(r) => { + if let Some(float_spec) = spec.float_spec() { + Ok(fmt_float(*r, float_spec)) + } else { + Err(Error::new(TypeMismatch::InterpolateSpec(self.type_of()))) + } + }, _ => Err(Error::new(TypeMismatch::Interpolate(self.type_of()))), } } @@ -822,7 +1113,6 @@ impl ObjectVariant { /// method implements equality under Gold semantics. pub fn user_eq(&self, other: &Self) -> bool { match (self, other) { - // Equality between disparate types (Self::Float(x), Self::Int(y)) => y.eq(x), (Self::Int(x), Self::Float(y)) => x.eq(y), @@ -1493,5 +1783,3 @@ impl From for Object { Object::key(value) } } - - diff --git a/gold/src/parsing.rs b/gold/src/parsing.rs index a989137..017a75b 100644 --- a/gold/src/parsing.rs +++ b/gold/src/parsing.rs @@ -606,10 +606,10 @@ fn fmtspec_char<'a>(c: char) -> impl Parser<'a, ()> { /// Matches a format specifier number. -fn fmtspec_number<'a>(input: In<'a>) -> Out<'a, u32> { +fn fmtspec_number<'a>(input: In<'a>) -> Out<'a, usize> { map_res( fmtspec_number_raw, - |out| out.as_ref().parse::(), + |out| out.as_ref().parse::(), )(input) } @@ -617,9 +617,9 @@ fn fmtspec_number<'a>(input: In<'a>) -> Out<'a, u32> { /// Matches a format specifier alignment. fn fmtspec_align<'a>(input: In<'a>) -> Out<'a, AlignSpec> { alt(( - value(AlignSpec::Left, fmtspec_char('<')), - value(AlignSpec::Right, fmtspec_char('>')), - value(AlignSpec::Center, fmtspec_char('^')), + value(AlignSpec::String(StringAlignSpec::Left), fmtspec_char('<')), + value(AlignSpec::String(StringAlignSpec::Right), fmtspec_char('>')), + value(AlignSpec::String(StringAlignSpec::Center), fmtspec_char('^')), value(AlignSpec::AfterSign, fmtspec_char('=')), ))(input) } @@ -664,20 +664,18 @@ fn fmtspec_type<'a>(input: In<'a>) -> Out<'a, FormatType> { alt(( value(FormatType::String, fmtspec_char('s')), - value(FormatType::Binary, fmtspec_char('b')), - value(FormatType::Character, fmtspec_char('c')), - value(FormatType::Decimal, fmtspec_char('d')), - value(FormatType::Octal, fmtspec_char('o')), - value(FormatType::Hex(UppercaseSpec::Lower), fmtspec_char('x')), - value(FormatType::Hex(UppercaseSpec::Upper), fmtspec_char('X')), - - value(FormatType::Sci(UppercaseSpec::Lower), fmtspec_char('e')), - value(FormatType::Sci(UppercaseSpec::Upper), fmtspec_char('E')), - value(FormatType::Fixed(UppercaseSpec::Lower), fmtspec_char('f')), - value(FormatType::Fixed(UppercaseSpec::Upper), fmtspec_char('F')), - value(FormatType::General(UppercaseSpec::Lower), fmtspec_char('g')), - value(FormatType::General(UppercaseSpec::Upper), fmtspec_char('G')), - value(FormatType::Percentage, fmtspec_char('%')), + value(FormatType::Integer(IntegerFormatType::Binary), fmtspec_char('b')), + value(FormatType::Integer(IntegerFormatType::Character), fmtspec_char('c')), + value(FormatType::Integer(IntegerFormatType::Decimal), fmtspec_char('d')), + value(FormatType::Integer(IntegerFormatType::Octal), fmtspec_char('o')), + value(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Lower)), fmtspec_char('x')), + value(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Upper)), fmtspec_char('X')), + + value(FormatType::Float(FloatFormatType::Sci(UppercaseSpec::Lower)), fmtspec_char('e')), + value(FormatType::Float(FloatFormatType::Sci(UppercaseSpec::Upper)), fmtspec_char('E')), + value(FormatType::Float(FloatFormatType::Fixed), fmtspec_char('f')), + value(FormatType::Float(FloatFormatType::General), fmtspec_char('g')), + value(FormatType::Float(FloatFormatType::Percentage), fmtspec_char('%')), ))(input) } @@ -696,18 +694,25 @@ fn format_specifier<'a>(input: In<'a>) -> Out<'a, FormatSpec> { opt(fmtspec_type), )), - |(fill_align, sign, alternate, zero, width, grouping, precision, fmt_type)| FormatSpec { - fill: match fill_align { - None => ' ', - Some((None, _)) => ' ', - Some((Some(fill), _)) => fill, - }, + |(fill_align, sign, alternate, zero_in, width, grouping, precision, fmt_type)| { + let zero = zero_in.unwrap_or_default(); + FormatSpec { + fill: match fill_align { + None => if zero { '0' } else {' '}, + Some((None, _)) => ' ', + Some((Some(fill), _)) => fill, + }, - align: fill_align.map(|(_, align)| align), - alternate: alternate.unwrap_or(false), - zero: zero.unwrap_or(false), + align: match (fill_align, zero) { + (Some((_, align)), _) => Some(align), //fill_align.map(|(_, align)| align) + (None, true) => Some(AlignSpec::AfterSign), + _ => None, + }, - sign, width, grouping, precision, fmt_type, + alternate: alternate.unwrap_or_default(), + + sign, width, grouping, precision, fmt_type, + } } )(input) } diff --git a/gold/src/tests/object.rs b/gold/src/tests/object.rs index 1d7aa1f..90bf970 100644 --- a/gold/src/tests/object.rs +++ b/gold/src/tests/object.rs @@ -1,6 +1,6 @@ use core::cmp::Ordering; -use crate::object::Object; +use crate::{object::Object, ast::{FormatSpec, AlignSpec, SignSpec, IntegerFormatType, FormatType, UppercaseSpec, GroupingSpec, FloatFormatType}}; #[test] @@ -35,9 +35,612 @@ fn to_string() { #[test] fn format() { - assert_eq!(Object::str("alpha").format(), Ok("alpha".to_string())); - assert_eq!(Object::str("\"alpha\"").format(), Ok("\"alpha\"".to_string())); - assert_eq!(Object::str("\"al\\pha\"").format(), Ok("\"al\\pha\"".to_string())); + assert_eq!(Object::str("alpha").format(Default::default()), Ok("alpha".to_string())); + assert_eq!(Object::str("\"alpha\"").format(Default::default()), Ok("\"alpha\"".to_string())); + assert_eq!(Object::str("\"al\\pha\"").format(Default::default()), Ok("\"al\\pha\"".to_string())); + assert_eq!(Object::bool(true).format(Default::default()), Ok("true".to_string())); + assert_eq!(Object::bool(false).format(Default::default()), Ok("false".to_string())); + assert_eq!(Object::null().format(Default::default()), Ok("null".to_string())); + assert_eq!(Object::int(0).format(Default::default()), Ok("0".to_string())); + assert_eq!(Object::int(-2).format(Default::default()), Ok("-2".to_string())); + assert_eq!(Object::int(5).format(Default::default()), Ok("5".to_string())); + + assert_eq!( + Object::str("dong").format( + FormatSpec { fill: ' ', width: Some(10), ..Default::default() } + ), + Ok("dong ".to_string()), + ); + + assert_eq!( + Object::str("dong").format( + FormatSpec { fill: ' ', width: Some(2), ..Default::default() } + ), + Ok("dong".to_string()), + ); + + assert_eq!( + Object::str("dong").format( + FormatSpec { fill: ' ', width: Some(12), align: Some(AlignSpec::left()), ..Default::default() } + ), + Ok("dong ".to_string()), + ); + + assert_eq!( + Object::str("dong").format( + FormatSpec { fill: ' ', width: Some(8), align: Some(AlignSpec::right()), ..Default::default() } + ), + Ok(" dong".to_string()), + ); + + assert_eq!( + Object::str("dong").format( + FormatSpec { fill: ' ', width: Some(8), align: Some(AlignSpec::center()), ..Default::default() } + ), + Ok(" dong ".to_string()), + ); + + assert_eq!( + Object::str("dong").format( + FormatSpec { fill: ' ', width: Some(7), align: Some(AlignSpec::center()), ..Default::default() } + ), + Ok(" dong ".to_string()), + ); + + assert_eq!( + Object::str("dong").format( + FormatSpec { fill: '~', width: Some(8), align: Some(AlignSpec::center()), ..Default::default() } + ), + Ok("~~dong~~".to_string()), + ); + + assert_eq!( + Object::bool(true).format( + FormatSpec { fill: '~', width: Some(8), align: Some(AlignSpec::center()), ..Default::default() } + ), + Ok("~~true~~".to_string()), + ); + + assert_eq!( + Object::bool(false).format( + FormatSpec { fmt_type: Some(FormatType::Integer(IntegerFormatType::Decimal)), ..Default::default() } + ), + Ok("0".to_string()), + ); + + assert_eq!( + Object::bool(true).format( + FormatSpec { fmt_type: Some(FormatType::Integer(IntegerFormatType::Decimal)), ..Default::default() } + ), + Ok("1".to_string()), + ); + + assert_eq!( + Object::bool(false).format( + FormatSpec { fill: ' ', width: Some(6), align: Some(AlignSpec::right()), ..Default::default() } + ), + Ok(" false".to_string()), + ); + + assert_eq!( + Object::null().format( + FormatSpec { fill: ' ', width: Some(6), align: Some(AlignSpec::center()), ..Default::default() } + ), + Ok(" null ".to_string()), + ); + + assert_eq!( + Object::int(0).format( + FormatSpec { + sign: Some(SignSpec::Plus), + ..Default::default() + } + ), + Ok("+0".to_string()), + ); + + assert_eq!( + Object::int(15).format( + FormatSpec { + sign: Some(SignSpec::Space), + ..Default::default() + } + ), + Ok(" 15".to_string()), + ); + + assert_eq!( + Object::int(11).format( + FormatSpec { + sign: Some(SignSpec::Minus), + ..Default::default() + } + ), + Ok("11".to_string()), + ); + + assert_eq!( + Object::int(-1).format( + FormatSpec { + sign: Some(SignSpec::Plus), + ..Default::default() + } + ), + Ok("-1".to_string()), + ); + + assert_eq!( + Object::int(-13).format( + FormatSpec { + sign: Some(SignSpec::Space), + ..Default::default() + } + ), + Ok("-13".to_string()), + ); + + assert_eq!( + Object::int(-10).format( + FormatSpec { + sign: Some(SignSpec::Minus), + ..Default::default() + } + ), + Ok("-10".to_string()), + ); + + assert_eq!( + Object::int(15).format( + FormatSpec { + align: Some(AlignSpec::left()), + width: Some(10), + ..Default::default() + } + ), + Ok("15 ".to_string()), + ); + + assert_eq!( + Object::int(15).format( + FormatSpec { + align: Some(AlignSpec::center()), + width: Some(10), + ..Default::default() + } + ), + Ok(" 15 ".to_string()), + ); + + assert_eq!( + Object::int(15).format( + FormatSpec { + align: Some(AlignSpec::right()), + width: Some(10), + ..Default::default() + } + ), + Ok(" 15".to_string()), + ); + + assert_eq!( + Object::int(-15).format( + FormatSpec { + align: Some(AlignSpec::left()), + width: Some(10), + ..Default::default() + } + ), + Ok("-15 ".to_string()), + ); + + assert_eq!( + Object::int(-15).format( + FormatSpec { + align: Some(AlignSpec::center()), + width: Some(10), + ..Default::default() + } + ), + Ok(" -15 ".to_string()), + ); + + assert_eq!( + Object::int(-15).format( + FormatSpec { + align: Some(AlignSpec::right()), + width: Some(10), + ..Default::default() + } + ), + Ok(" -15".to_string()), + ); + + assert_eq!( + Object::int(-15).format( + FormatSpec { + align: Some(AlignSpec::AfterSign), + width: Some(10), + ..Default::default() + } + ), + Ok("- 15".to_string()), + ); + + assert_eq!( + Object::int(15).format( + FormatSpec { + align: Some(AlignSpec::AfterSign), + width: Some(10), + ..Default::default() + } + ), + Ok(" 15".to_string()), + ); + + assert_eq!( + Object::int(23).format( + FormatSpec { + fmt_type: Some(FormatType::Integer(IntegerFormatType::Decimal)), + ..Default::default() + } + ), + Ok("23".to_string()), + ); + + assert_eq!( + Object::int(23).format( + FormatSpec { + fmt_type: Some(FormatType::Integer(IntegerFormatType::Binary)), + ..Default::default() + } + ), + Ok("10111".to_string()), + ); + + assert_eq!( + Object::int(23).format( + FormatSpec { + fmt_type: Some(FormatType::Integer(IntegerFormatType::Octal)), + ..Default::default() + } + ), + Ok("27".to_string()), + ); + + assert_eq!( + Object::int(42).format( + FormatSpec { + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Lower))), + ..Default::default() + } + ), + Ok("2a".to_string()), + ); + + assert_eq!( + Object::int(42).format( + FormatSpec { + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Upper))), + ..Default::default() + } + ), + Ok("2A".to_string()), + ); + + assert_eq!( + Object::int(23).format( + FormatSpec { + alternate: true, + fmt_type: Some(FormatType::Integer(IntegerFormatType::Decimal)), + ..Default::default() + } + ), + Ok("23".to_string()), + ); + + assert_eq!( + Object::int(23).format( + FormatSpec { + alternate: true, + fmt_type: Some(FormatType::Integer(IntegerFormatType::Binary)), + ..Default::default() + } + ), + Ok("0b10111".to_string()), + ); + + assert_eq!( + Object::int(23).format( + FormatSpec { + alternate: true, + fmt_type: Some(FormatType::Integer(IntegerFormatType::Octal)), + ..Default::default() + } + ), + Ok("0o27".to_string()), + ); + + assert_eq!( + Object::int(42).format( + FormatSpec { + alternate: true, + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Lower))), + ..Default::default() + } + ), + Ok("0x2a".to_string()), + ); + + assert_eq!( + Object::int(42).format( + FormatSpec { + alternate: true, + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Upper))), + ..Default::default() + } + ), + Ok("0X2A".to_string()), + ); + + assert_eq!( + Object::int(12738912).format( + FormatSpec { + grouping: Some(GroupingSpec::Comma), + fmt_type: Some(FormatType::Integer(IntegerFormatType::Decimal)), + ..Default::default() + } + ), + Ok("12,738,912".to_string()), + ); + + assert_eq!( + Object::int(12738912).format( + FormatSpec { + grouping: Some(GroupingSpec::Underscore), + fmt_type: Some(FormatType::Integer(IntegerFormatType::Binary)), + ..Default::default() + } + ), + Ok("1100_0010_0110_0001_0110_0000".to_string()), + ); + + assert_eq!( + Object::int(12738912).format( + FormatSpec { + grouping: Some(GroupingSpec::Underscore), + fmt_type: Some(FormatType::Integer(IntegerFormatType::Octal)), + ..Default::default() + } + ), + Ok("6046_0540".to_string()), + ); + + assert_eq!( + Object::int(12738912).format( + FormatSpec { + grouping: Some(GroupingSpec::Comma), + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Lower))), + ..Default::default() + } + ), + Ok("c2,6160".to_string()), + ); + + assert_eq!( + Object::int(12738912).format( + FormatSpec { + width: Some(12), + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Lower))), + ..Default::default() + } + ), + Ok(" c26160".to_string()), + ); + + assert_eq!( + Object::int(12738912).format( + FormatSpec { + sign: Some(SignSpec::Plus), + width: Some(12), + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Lower))), + ..Default::default() + } + ), + Ok(" +c26160".to_string()), + ); + + assert_eq!( + Object::int(12738912).format( + FormatSpec { + align: Some(AlignSpec::AfterSign), + sign: Some(SignSpec::Plus), + width: Some(12), + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Lower))), + ..Default::default() + } + ), + Ok("+ c26160".to_string()), + ); + + assert_eq!( + Object::int(12738912).format( + FormatSpec { + align: Some(AlignSpec::AfterSign), + sign: Some(SignSpec::Plus), + alternate: true, + width: Some(12), + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Lower))), + ..Default::default() + } + ), + Ok("+0x c26160".to_string()), + ); + + assert_eq!( + Object::int(12738912).format( + FormatSpec { + align: Some(AlignSpec::AfterSign), + sign: Some(SignSpec::Plus), + alternate: true, + width: Some(12), + grouping: Some(GroupingSpec::Underscore), + fmt_type: Some(FormatType::Integer(IntegerFormatType::Hex(UppercaseSpec::Lower))), + ..Default::default() + } + ), + Ok("+0x c2_6160".to_string()), + ); + + assert_eq!( + Object::float(1.234).format( + FormatSpec { + precision: Some(1), + ..Default::default() + } + ), + Ok("1.2".to_string()), + ); + + assert_eq!( + Object::float(1.234).format( + FormatSpec { + precision: Some(6), + ..Default::default() + } + ), + Ok("1.234000".to_string()), + ); + + assert_eq!( + Object::float(1.234).format( + FormatSpec { + fmt_type: Some(FormatType::Float(FloatFormatType::Fixed)), + ..Default::default() + } + ), + Ok("1.234000".to_string()), + ); + + assert_eq!( + Object::float(1.234).format( + FormatSpec { + precision: Some(9), + fmt_type: Some(FormatType::Float(FloatFormatType::Fixed)), + ..Default::default() + } + ), + Ok("1.234000000".to_string()), + ); + + assert_eq!( + Object::float(12.34).format( + FormatSpec { + precision: Some(5), + fmt_type: Some(FormatType::Float(FloatFormatType::Sci(UppercaseSpec::Lower))), + ..Default::default() + } + ), + Ok("1.23400e1".to_string()), + ); + + assert_eq!( + Object::float(12.34).format( + FormatSpec { + fmt_type: Some(FormatType::Float(FloatFormatType::Sci(UppercaseSpec::Upper))), + ..Default::default() + } + ), + Ok("1.234000E1".to_string()), + ); + + assert_eq!( + Object::float(12.34).format( + FormatSpec { + fmt_type: Some(FormatType::Float(FloatFormatType::General)), + ..Default::default() + } + ), + Ok("12.34".to_string()), + ); + + assert_eq!( + Object::float(12.34).format( + FormatSpec { + align: Some(AlignSpec::AfterSign), + width: Some(8), + ..Default::default() + } + ), + Ok(" 12.34".to_string()), + ); + + assert_eq!( + Object::float(-12.34).format( + FormatSpec { + align: Some(AlignSpec::AfterSign), + width: Some(8), + ..Default::default() + } + ), + Ok("- 12.34".to_string()), + ); + + assert_eq!( + Object::float(12.34).format( + FormatSpec { + align: Some(AlignSpec::AfterSign), + sign: Some(SignSpec::Plus), + width: Some(8), + ..Default::default() + } + ), + Ok("+ 12.34".to_string()), + ); + + assert_eq!( + Object::float(12.34).format( + FormatSpec { + align: Some(AlignSpec::left()), + sign: Some(SignSpec::Plus), + width: Some(8), + ..Default::default() + } + ), + Ok("+12.34 ".to_string()), + ); + + assert_eq!( + Object::float(12.34).format( + FormatSpec { + align: Some(AlignSpec::center()), + sign: Some(SignSpec::Plus), + width: Some(8), + ..Default::default() + } + ), + Ok(" +12.34 ".to_string()), + ); + + assert_eq!( + Object::float(1000000.0).format( + FormatSpec { + grouping: Some(GroupingSpec::Underscore), + ..Default::default() + } + ), + Ok("1_000_000".to_string()), + ); + + assert_eq!( + Object::float(1000000.0).format( + FormatSpec { + grouping: Some(GroupingSpec::Underscore), + precision: Some(8), + ..Default::default() + } + ), + Ok("1_000_000.00000000".to_string()), + ); } diff --git a/gold/src/tests/parsing.rs b/gold/src/tests/parsing.rs index 26b1922..ba58522 100644 --- a/gold/src/tests/parsing.rs +++ b/gold/src/tests/parsing.rs @@ -155,7 +155,6 @@ fn string_format() { align: None, sign: None, alternate: false, - zero: false, width: None, grouping: None, precision: None, @@ -178,7 +177,7 @@ fn string_format() { StringElement::Interpolate( "a".id(3), Some(FormatSpec { - align: Some(AlignSpec::Right), + align: Some(AlignSpec::String(StringAlignSpec::Right)), sign: Some(SignSpec::Plus), width: Some(30), ..Default::default() @@ -191,7 +190,7 @@ fn string_format() { "a".id(3), Some(FormatSpec { fill: '$', - align: Some(AlignSpec::Center), + align: Some(AlignSpec::String(StringAlignSpec::Center)), alternate: true, precision: Some(3), ..Default::default() @@ -203,7 +202,8 @@ fn string_format() { StringElement::Interpolate( "a".id(3), Some(FormatSpec { - zero: true, + fill: '0', + align: Some(AlignSpec::AfterSign), grouping: Some(GroupingSpec::Comma), precision: Some(5), fmt_type: Some(FormatType::String),