12#![allow(clippy::too_many_arguments)]34use jrsonnet_gcmodule::Trace;5use jrsonnet_interner::IStr;6use jrsonnet_types::ValType;7use thiserror::Error;89use crate::{error::Error::*, throw, typed::Typed, LocError, ObjValue, Result, State, Val};1011#[derive(Debug, Clone, Error, Trace)]12pub enum FormatError {13 #[error("truncated format code")]14 TruncatedFormatCode,15 #[error("unrecognized conversion type: {0}")]16 UnrecognizedConversionType(char),1718 #[error("not enough values")]19 NotEnoughValues,2021 #[error("cannot use * width with object")]22 CannotUseStarWidthWithObject,23 #[error("mapping keys required")]24 MappingKeysRequired,25 #[error("no such format field: {0}")]26 NoSuchFormatField(IStr),27}2829impl From<FormatError> for LocError {30 fn from(e: FormatError) -> Self {31 Self::new(Format(e))32 }33}3435use FormatError::*;3637type ParseResult<'t, T> = std::result::Result<(T, &'t str), FormatError>;3839pub fn try_parse_mapping_key(str: &str) -> ParseResult<'_, &str> {40 if str.is_empty() {41 return Err(TruncatedFormatCode);42 }43 let bytes = str.as_bytes();44 if bytes[0] == b'(' {45 let mut i = 1;46 while i < bytes.len() {47 if bytes[i] == b')' {48 return Ok((&str[1..i], &str[i + 1..]));49 }50 i += 1;51 }52 Err(TruncatedFormatCode)53 } else {54 Ok(("", str))55 }56}5758#[cfg(test)]59pub mod tests_key {60 use super::*;6162 #[test]63 fn parse_key() {64 assert_eq!(65 try_parse_mapping_key("(hello ) world").unwrap(),66 ("hello ", " world")67 );68 assert_eq!(try_parse_mapping_key("() world").unwrap(), ("", " world"));69 assert_eq!(try_parse_mapping_key(" world").unwrap(), ("", " world"));70 assert_eq!(71 try_parse_mapping_key(" () world").unwrap(),72 ("", " () world")73 );74 }7576 #[test]77 #[should_panic]78 fn parse_key_missing_start() {79 try_parse_mapping_key("").unwrap();80 }8182 #[test]83 #[should_panic]84 fn parse_key_missing_end() {85 try_parse_mapping_key("( ").unwrap();86 }87}8889#[allow(clippy::struct_excessive_bools)]90#[derive(Default, Debug)]91pub struct CFlags {92 pub alt: bool,93 pub zero: bool,94 pub left: bool,95 pub blank: bool,96 pub sign: bool,97}9899pub fn try_parse_cflags(str: &str) -> ParseResult<'_, CFlags> {100 if str.is_empty() {101 return Err(TruncatedFormatCode);102 }103 let bytes = str.as_bytes();104 let mut i = 0;105 let mut out = CFlags::default();106 loop {107 if bytes.len() == i {108 return Err(TruncatedFormatCode);109 }110 match bytes[i] {111 b'#' => out.alt = true,112 b'0' => out.zero = true,113 b'-' => out.left = true,114 b' ' => out.blank = true,115 b'+' => out.sign = true,116 _ => break,117 }118 i += 1;119 }120 Ok((out, &str[i..]))121}122123#[derive(Debug, PartialEq, Eq)]124pub enum Width {125 Star,126 Fixed(usize),127}128pub fn try_parse_field_width(str: &str) -> ParseResult<'_, Width> {129 if str.is_empty() {130 return Err(TruncatedFormatCode);131 }132 let bytes = str.as_bytes();133 if bytes[0] == b'*' {134 return Ok((Width::Star, &str[1..]));135 }136 let mut out: usize = 0;137 let mut digits = 0;138 while let Some(digit) = (bytes[digits] as char).to_digit(10) {139 out *= 10;140 out += digit as usize;141 digits += 1;142 if digits == bytes.len() {143 return Err(TruncatedFormatCode);144 }145 }146 Ok((Width::Fixed(out), &str[digits..]))147}148149pub fn try_parse_precision(str: &str) -> ParseResult<'_, Option<Width>> {150 if str.is_empty() {151 return Err(TruncatedFormatCode);152 }153 let bytes = str.as_bytes();154 if bytes[0] == b'.' {155 try_parse_field_width(&str[1..]).map(|(r, s)| (Some(r), s))156 } else {157 Ok((None, str))158 }159}160161162pub fn try_parse_length_modifier(str: &str) -> ParseResult<'_, ()> {163 if str.is_empty() {164 return Err(TruncatedFormatCode);165 }166 let bytes = str.as_bytes();167 let mut idx = 0;168 while bytes[idx] == b'h' || bytes[idx] == b'l' || bytes[idx] == b'L' {169 idx += 1;170 if bytes.len() == idx {171 return Err(TruncatedFormatCode);172 }173 }174 Ok(((), &str[idx..]))175}176177#[derive(Debug, PartialEq, Eq)]178pub enum ConvTypeV {179 Decimal,180 Octal,181 Hexadecimal,182 Scientific,183 Float,184 Shorter,185 Char,186 String,187 Percent,188}189pub struct ConvType {190 v: ConvTypeV,191 caps: bool,192}193194pub fn parse_conversion_type(str: &str) -> ParseResult<'_, ConvType> {195 if str.is_empty() {196 return Err(TruncatedFormatCode);197 }198199 let code = str.as_bytes()[0];200 let v: (ConvTypeV, bool) = match code {201 b'd' | b'i' | b'u' => (ConvTypeV::Decimal, false),202 b'o' => (ConvTypeV::Octal, false),203 b'x' => (ConvTypeV::Hexadecimal, false),204 b'X' => (ConvTypeV::Hexadecimal, true),205 b'e' => (ConvTypeV::Scientific, false),206 b'E' => (ConvTypeV::Scientific, true),207 b'f' => (ConvTypeV::Float, false),208 b'F' => (ConvTypeV::Float, true),209 b'g' => (ConvTypeV::Shorter, false),210 b'G' => (ConvTypeV::Shorter, true),211 b'c' => (ConvTypeV::Char, false),212 b's' => (ConvTypeV::String, false),213 b'%' => (ConvTypeV::Percent, false),214 c => return Err(UnrecognizedConversionType(c as char)),215 };216217 Ok((ConvType { v: v.0, caps: v.1 }, &str[1..]))218}219220#[derive(Debug)]221pub struct Code<'s> {222 mkey: &'s str,223 cflags: CFlags,224 width: Width,225 precision: Option<Width>,226 convtype: ConvTypeV,227 caps: bool,228}229pub fn parse_code(str: &str) -> ParseResult<'_, Code<'_>> {230 if str.is_empty() {231 return Err(TruncatedFormatCode);232 }233 let (mkey, str) = try_parse_mapping_key(str)?;234 let (cflags, str) = try_parse_cflags(str)?;235 let (width, str) = try_parse_field_width(str)?;236 let (precision, str) = try_parse_precision(str)?;237 let (_, str) = try_parse_length_modifier(str)?;238 let (convtype, str) = parse_conversion_type(str)?;239240 Ok((241 Code {242 mkey,243 cflags,244 width,245 precision,246 convtype: convtype.v,247 caps: convtype.caps,248 },249 str,250 ))251}252253#[derive(Debug)]254pub enum Element<'s> {255 String(&'s str),256 Code(Code<'s>),257}258pub fn parse_codes(mut str: &str) -> Result<Vec<Element<'_>>> {259 let mut bytes = str.as_bytes();260 let mut out = vec![];261 let mut offset = 0;262263 loop {264 while offset != bytes.len() && bytes[offset] != b'%' {265 offset += 1;266 }267 if offset != 0 {268 out.push(Element::String(&str[0..offset]));269 }270 if offset == bytes.len() {271 return Ok(out);272 }273 str = &str[offset + 1..];274 let code;275 (code, str) = parse_code(str)?;276 bytes = str.as_bytes();277 offset = 0;278279 out.push(Element::Code(code));280 }281}282283const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";284285#[inline]286pub fn render_integer(287 out: &mut String,288 iv: f64,289 padding: usize,290 precision: usize,291 blank: bool,292 sign: bool,293 radix: i64,294 prefix: &str,295 caps: bool,296) {297 let radix = radix as f64;298 let iv = iv.floor();299 300 301 let digits = if iv == 0.0 {302 vec![0u8]303 } else {304 let mut v = iv.abs();305 let mut nums = Vec::with_capacity(1);306 while v != 0.0 {307 nums.push((v % radix) as u8);308 v = (v / radix).floor();309 }310 nums311 };312 let neg = iv < 0.0;313 #[allow(clippy::bool_to_int_with_if)]314 let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 });315 let zp2 = zp316 .max(precision)317 .saturating_sub(prefix.len() + digits.len());318319 if neg {320 out.push('-');321 } else if sign {322 out.push('+');323 } else if blank {324 out.push(' ');325 }326327 out.reserve(zp2);328 for _ in 0..zp2 {329 out.push('0');330 }331 out.push_str(prefix);332333 for digit in digits.into_iter().rev() {334 let ch = NUMBERS[digit as usize] as char;335 out.push(if caps { ch.to_ascii_uppercase() } else { ch });336 }337}338339pub fn render_decimal(340 out: &mut String,341 iv: f64,342 padding: usize,343 precision: usize,344 blank: bool,345 sign: bool,346) {347 render_integer(out, iv, padding, precision, blank, sign, 10, "", false);348}349pub fn render_octal(350 out: &mut String,351 iv: f64,352 padding: usize,353 precision: usize,354 alt: bool,355 blank: bool,356 sign: bool,357) {358 render_integer(359 out,360 iv,361 padding,362 precision,363 blank,364 sign,365 8,366 if alt && iv != 0.0 { "0" } else { "" },367 false,368 );369}370371#[allow(clippy::fn_params_excessive_bools)]372pub fn render_hexadecimal(373 out: &mut String,374 iv: f64,375 padding: usize,376 precision: usize,377 alt: bool,378 blank: bool,379 sign: bool,380 caps: bool,381) {382 render_integer(383 out,384 iv,385 padding,386 precision,387 blank,388 sign,389 16,390 match (alt, caps) {391 (true, true) => "0X",392 (true, false) => "0x",393 (false, _) => "",394 },395 caps,396 );397}398399#[allow(clippy::fn_params_excessive_bools)]400pub fn render_float(401 out: &mut String,402 n: f64,403 mut padding: usize,404 precision: usize,405 blank: bool,406 sign: bool,407 ensure_pt: bool,408 trailing: bool,409) {410 #[allow(clippy::bool_to_int_with_if)]411 let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 };412 padding = padding.saturating_sub(dot_size + precision);413 render_decimal(out, n.floor(), padding, 0, blank, sign);414 if precision == 0 {415 if ensure_pt {416 out.push('.');417 }418 return;419 }420 let frac = n421 .fract()422 .mul_add(10.0_f64.powf(precision as f64), 0.5)423 .floor();424 if trailing || frac > 0.0 {425 out.push('.');426 let mut frac_str = String::new();427 render_decimal(&mut frac_str, frac, precision, 0, false, false);428 let mut trim = frac_str.len();429 if !trailing {430 for b in frac_str.as_bytes().iter().rev() {431 if *b == b'0' {432 trim -= 1;433 }434 }435 }436 out.push_str(&frac_str[..trim]);437 } else if ensure_pt {438 out.push('.');439 }440}441442#[allow(clippy::fn_params_excessive_bools)]443pub fn render_float_sci(444 out: &mut String,445 n: f64,446 mut padding: usize,447 precision: usize,448 blank: bool,449 sign: bool,450 ensure_pt: bool,451 trailing: bool,452 caps: bool,453) {454 let exponent = n.log10().floor();455 let mantissa = if exponent as i16 == -324 {456 n * 10.0 / 10.0_f64.powf(exponent + 1.0)457 } else {458 n / 10.0_f64.powf(exponent)459 };460 let mut exponent_str = String::new();461 render_decimal(&mut exponent_str, exponent, 3, 0, false, true);462463 464 padding = padding.saturating_sub(exponent_str.len() + 1);465466 render_float(467 out, mantissa, padding, precision, blank, sign, ensure_pt, trailing,468 );469 out.push(if caps { 'E' } else { 'e' });470 out.push_str(&exponent_str);471}472473#[allow(clippy::too_many_lines)]474pub fn format_code(475 s: State,476 out: &mut String,477 value: &Val,478 code: &Code<'_>,479 width: usize,480 precision: Option<usize>,481) -> Result<()> {482 let clfags = &code.cflags;483 let (fpprec, iprec) = precision.map_or((6, 0), |v| (v, v));484 let padding = if clfags.zero && !clfags.left {485 width486 } else {487 0488 };489490 491 let mut tmp_out = String::new();492493 match code.convtype {494 ConvTypeV::String => tmp_out.push_str(&value.clone().to_string(s)?),495 ConvTypeV::Decimal => {496 let value = f64::from_untyped(value.clone(), s)?;497 render_decimal(498 &mut tmp_out,499 value,500 padding,501 iprec,502 clfags.blank,503 clfags.sign,504 );505 }506 ConvTypeV::Octal => {507 let value = f64::from_untyped(value.clone(), s)?;508 render_octal(509 &mut tmp_out,510 value,511 padding,512 iprec,513 clfags.alt,514 clfags.blank,515 clfags.sign,516 );517 }518 ConvTypeV::Hexadecimal => {519 let value = f64::from_untyped(value.clone(), s)?;520 render_hexadecimal(521 &mut tmp_out,522 value,523 padding,524 iprec,525 clfags.alt,526 clfags.blank,527 clfags.sign,528 code.caps,529 );530 }531 ConvTypeV::Scientific => {532 let value = f64::from_untyped(value.clone(), s)?;533 render_float_sci(534 &mut tmp_out,535 value,536 padding,537 fpprec,538 clfags.blank,539 clfags.sign,540 clfags.alt,541 true,542 code.caps,543 );544 }545 ConvTypeV::Float => {546 let value = f64::from_untyped(value.clone(), s)?;547 render_float(548 &mut tmp_out,549 value,550 padding,551 fpprec,552 clfags.blank,553 clfags.sign,554 clfags.alt,555 true,556 );557 }558 ConvTypeV::Shorter => {559 let value = f64::from_untyped(value.clone(), s)?;560 let exponent = value.log10().floor();561 if exponent < -4.0 || exponent >= fpprec as f64 {562 render_float_sci(563 &mut tmp_out,564 value,565 padding,566 fpprec - 1,567 clfags.blank,568 clfags.sign,569 clfags.alt,570 clfags.alt,571 code.caps,572 );573 } else {574 let digits_before_pt = 1.max(exponent as usize + 1);575 render_float(576 &mut tmp_out,577 value,578 padding,579 fpprec - digits_before_pt,580 clfags.blank,581 clfags.sign,582 clfags.alt,583 clfags.alt,584 );585 }586 }587 ConvTypeV::Char => match value.clone() {588 Val::Num(n) => tmp_out.push(589 std::char::from_u32(n as u32)590 .ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?,591 ),592 Val::Str(s) => {593 if s.chars().count() != 1 {594 throw!(RuntimeError(595 format!("%c expected 1 char string, got {}", s.chars().count()).into(),596 ));597 }598 tmp_out.push_str(&s);599 }600 _ => {601 throw!(TypeMismatch(602 "%c requires number/string",603 vec![ValType::Num, ValType::Str],604 value.value_type(),605 ));606 }607 },608 ConvTypeV::Percent => tmp_out.push('%'),609 };610611 let padding = width.saturating_sub(tmp_out.len());612613 if !clfags.left {614 for _ in 0..padding {615 out.push(' ');616 }617 }618 out.push_str(&tmp_out);619 if clfags.left {620 for _ in 0..padding {621 out.push(' ');622 }623 }624625 Ok(())626}627628pub fn format_arr(s: State, str: &str, mut values: &[Val]) -> Result<String> {629 let codes = parse_codes(str)?;630 let mut out = String::new();631632 for code in codes {633 match code {634 Element::String(s) => {635 out.push_str(s);636 }637 Element::Code(c) => {638 let width = match c.width {639 Width::Star => {640 if values.is_empty() {641 throw!(NotEnoughValues);642 }643 let value = &values[0];644 values = &values[1..];645 usize::from_untyped(value.clone(), s.clone())?646 }647 Width::Fixed(n) => n,648 };649 let precision = match c.precision {650 Some(Width::Star) => {651 if values.is_empty() {652 throw!(NotEnoughValues);653 }654 let value = &values[0];655 values = &values[1..];656 Some(usize::from_untyped(value.clone(), s.clone())?)657 }658 Some(Width::Fixed(n)) => Some(n),659 None => None,660 };661662 663 let value = if c.convtype == ConvTypeV::Percent {664 &Val::Null665 } else {666 if values.is_empty() {667 throw!(NotEnoughValues);668 }669 let value = &values[0];670 values = &values[1..];671 value672 };673674 format_code(s.clone(), &mut out, value, &c, width, precision)?;675 }676 }677 }678679 Ok(out)680}681682pub fn format_obj(s: State, str: &str, values: &ObjValue) -> Result<String> {683 let codes = parse_codes(str)?;684 let mut out = String::new();685686 for code in codes {687 match code {688 Element::String(s) => {689 out.push_str(s);690 }691 Element::Code(c) => {692 693 let f: IStr = c.mkey.into();694 let width = match c.width {695 Width::Star => {696 throw!(CannotUseStarWidthWithObject);697 }698 Width::Fixed(n) => n,699 };700 let precision = match c.precision {701 Some(Width::Star) => {702 throw!(CannotUseStarWidthWithObject);703 }704 Some(Width::Fixed(n)) => Some(n),705 None => None,706 };707708 let value = if c.convtype == ConvTypeV::Percent {709 Val::Null710 } else {711 if f.is_empty() {712 throw!(MappingKeysRequired);713 }714 if let Some(v) = values.get(s.clone(), f.clone())? {715 v716 } else {717 throw!(NoSuchFormatField(f));718 }719 };720721 format_code(s.clone(), &mut out, &value, &c, width, precision)?;722 }723 }724 }725726 Ok(out)727}728729#[cfg(test)]730pub mod test_format {731 use super::*;732733 #[test]734 fn parse() {735 assert_eq!(736 parse_codes(737 "How much error budget is left looking at our %.3f%% availability gurantees?"738 )739 .unwrap()740 .len(),741 4742 );743 }744745 #[test]746 fn octals() {747 let s = State::default();748 assert_eq!(749 format_arr(s.clone(), "%#o", &[Val::Num(8.0)]).unwrap(),750 "010"751 );752 assert_eq!(753 format_arr(s.clone(), "%#4o", &[Val::Num(8.0)]).unwrap(),754 " 010"755 );756 assert_eq!(757 format_arr(s.clone(), "%4o", &[Val::Num(8.0)]).unwrap(),758 " 10"759 );760 assert_eq!(761 format_arr(s.clone(), "%04o", &[Val::Num(8.0)]).unwrap(),762 "0010"763 );764 assert_eq!(765 format_arr(s.clone(), "%+4o", &[Val::Num(8.0)]).unwrap(),766 " +10"767 );768 assert_eq!(769 format_arr(s.clone(), "%+04o", &[Val::Num(8.0)]).unwrap(),770 "+010"771 );772 assert_eq!(773 format_arr(s.clone(), "%-4o", &[Val::Num(8.0)]).unwrap(),774 "10 "775 );776 assert_eq!(777 format_arr(s.clone(), "%+-4o", &[Val::Num(8.0)]).unwrap(),778 "+10 "779 );780 assert_eq!(format_arr(s, "%+-04o", &[Val::Num(8.0)]).unwrap(), "+10 ");781 }782783 #[test]784 fn percent_doesnt_consumes_values() {785 let s = State::default();786 assert_eq!(787 format_arr(788 s,789 "How much error budget is left looking at our %.3f%% availability gurantees?",790 &[Val::Num(4.0)]791 )792 .unwrap(),793 "How much error budget is left looking at our 4.000% availability gurantees?"794 );795 }796}