git.delta.rocks / jrsonnet / refs/commits / df9bc99da41f

difftreelog

source

crates/jrsonnet-evaluator/src/builtin/format.rs15.5 KiBsourcehistory
1//! faster std.format impl2#![allow(clippy::too_many_arguments)]34use crate::{error::Error::*, throw, LocError, ObjValue, Result, Val};5use gcmodule::Trace;6use jrsonnet_interner::IStr;7use jrsonnet_types::ValType;8use thiserror::Error;910#[derive(Debug, Clone, Error, Trace)]11pub enum FormatError {12	#[error("truncated format code")]13	TruncatedFormatCode,14	#[error("unrecognized conversion type: {0}")]15	UnrecognizedConversionType(char),1617	#[error("not enough values")]18	NotEnoughValues,1920	#[error("cannot use * width with object")]21	CannotUseStarWidthWithObject,22	#[error("mapping keys required")]23	MappingKeysRequired,24	#[error("no such format field: {0}")]25	NoSuchFormatField(IStr),26}2728impl From<FormatError> for LocError {29	fn from(e: FormatError) -> Self {30		Self::new(Format(e))31	}32}3334use FormatError::*;3536type ParseResult<'t, T> = std::result::Result<(T, &'t str), FormatError>;3738pub fn try_parse_mapping_key(str: &str) -> ParseResult<&str> {39	if str.is_empty() {40		return Err(TruncatedFormatCode);41	}42	let bytes = str.as_bytes();43	if bytes[0] == b'(' {44		let mut i = 1;45		while i < bytes.len() {46			if bytes[i] == b')' {47				return Ok((&str[1..i as usize], &str[i as usize + 1..]));48			}49			i += 1;50		}51		Err(TruncatedFormatCode)52	} else {53		Ok(("", str))54	}55}5657#[cfg(test)]58pub mod tests_key {59	use super::*;6061	#[test]62	fn parse_key() {63		assert_eq!(64			try_parse_mapping_key("(hello ) world").unwrap(),65			("hello ", " world")66		);67		assert_eq!(try_parse_mapping_key("() world").unwrap(), ("", " world"));68		assert_eq!(try_parse_mapping_key(" world").unwrap(), ("", " world"));69		assert_eq!(70			try_parse_mapping_key(" () world").unwrap(),71			("", " () world")72		);73	}7475	#[test]76	#[should_panic]77	fn parse_key_missing_start() {78		try_parse_mapping_key("").unwrap();79	}8081	#[test]82	#[should_panic]83	fn parse_key_missing_end() {84		try_parse_mapping_key("(   ").unwrap();85	}86}8788#[derive(Default, Debug)]89pub struct CFlags {90	pub alt: bool,91	pub zero: bool,92	pub left: bool,93	pub blank: bool,94	pub sign: bool,95}9697pub fn try_parse_cflags(str: &str) -> ParseResult<CFlags> {98	if str.is_empty() {99		return Err(TruncatedFormatCode);100	}101	let bytes = str.as_bytes();102	let mut i = 0;103	let mut out = CFlags::default();104	loop {105		if bytes.len() == i {106			return Err(TruncatedFormatCode);107		}108		match bytes[i] {109			b'#' => out.alt = true,110			b'0' => out.zero = true,111			b'-' => out.left = true,112			b' ' => out.blank = true,113			b'+' => out.sign = true,114			_ => break,115		}116		i += 1;117	}118	Ok((out, &str[i..]))119}120121#[derive(Debug, PartialEq)]122pub enum Width {123	Star,124	Fixed(usize),125}126pub fn try_parse_field_width(str: &str) -> ParseResult<Width> {127	if str.is_empty() {128		return Err(TruncatedFormatCode);129	}130	let bytes = str.as_bytes();131	if bytes[0] == b'*' {132		return Ok((Width::Star, &str[1..]));133	}134	let mut out: usize = 0;135	let mut digits = 0;136	while let Some(digit) = (bytes[digits] as char).to_digit(10) {137		out *= 10;138		out += digit as usize;139		digits += 1;140		if digits == bytes.len() {141			return Err(TruncatedFormatCode);142		}143	}144	Ok((Width::Fixed(out), &str[digits..]))145}146147pub fn try_parse_precision(str: &str) -> ParseResult<Option<Width>> {148	if str.is_empty() {149		return Err(TruncatedFormatCode);150	}151	let bytes = str.as_bytes();152	if bytes[0] == b'.' {153		try_parse_field_width(&str[1..]).map(|(r, s)| (Some(r), s))154	} else {155		Ok((None, str))156	}157}158159// Only skips160pub fn try_parse_length_modifier(str: &str) -> ParseResult<()> {161	if str.is_empty() {162		return Err(TruncatedFormatCode);163	}164	let bytes = str.as_bytes();165	let mut idx = 0;166	while bytes[idx] == b'h' || bytes[idx] == b'l' || bytes[idx] == b'L' {167		idx += 1;168		if bytes.len() == idx {169			return Err(TruncatedFormatCode);170		}171	}172	Ok(((), &str[idx..]))173}174175#[derive(Debug, PartialEq)]176pub enum ConvTypeV {177	Decimal,178	Octal,179	Hexadecimal,180	Scientific,181	Float,182	Shorter,183	Char,184	String,185	Percent,186}187pub struct ConvType {188	v: ConvTypeV,189	caps: bool,190}191192pub fn parse_conversion_type(str: &str) -> ParseResult<ConvType> {193	if str.is_empty() {194		return Err(TruncatedFormatCode);195	}196197	let code = str.as_bytes()[0];198	let v: (ConvTypeV, bool) = match code {199		b'd' | b'i' | b'u' => (ConvTypeV::Decimal, false),200		b'o' => (ConvTypeV::Octal, false),201		b'x' => (ConvTypeV::Hexadecimal, false),202		b'X' => (ConvTypeV::Hexadecimal, true),203		b'e' => (ConvTypeV::Scientific, false),204		b'E' => (ConvTypeV::Scientific, true),205		b'f' => (ConvTypeV::Float, false),206		b'F' => (ConvTypeV::Float, true),207		b'g' => (ConvTypeV::Shorter, false),208		b'G' => (ConvTypeV::Shorter, true),209		b'c' => (ConvTypeV::Char, false),210		b's' => (ConvTypeV::String, false),211		b'%' => (ConvTypeV::Percent, false),212		c => return Err(UnrecognizedConversionType(c as char)),213	};214215	Ok((ConvType { v: v.0, caps: v.1 }, &str[1..]))216}217218#[derive(Debug)]219pub struct Code<'s> {220	mkey: &'s str,221	cflags: CFlags,222	width: Width,223	precision: Option<Width>,224	convtype: ConvTypeV,225	caps: bool,226}227pub fn parse_code(str: &str) -> ParseResult<Code> {228	if str.is_empty() {229		return Err(TruncatedFormatCode);230	}231	let (mkey, str) = try_parse_mapping_key(str)?;232	let (cflags, str) = try_parse_cflags(str)?;233	let (width, str) = try_parse_field_width(str)?;234	let (precision, str) = try_parse_precision(str)?;235	let (_, str) = try_parse_length_modifier(str)?;236	let (convtype, str) = parse_conversion_type(str)?;237238	Ok((239		Code {240			mkey,241			cflags,242			width,243			precision,244			convtype: convtype.v,245			caps: convtype.caps,246		},247		str,248	))249}250251#[derive(Debug)]252pub enum Element<'s> {253	String(&'s str),254	Code(Code<'s>),255}256pub fn parse_codes(mut str: &str) -> Result<Vec<Element>> {257	let mut bytes = str.as_bytes();258	let mut out = vec![];259	let mut offset = 0;260261	loop {262		while offset != bytes.len() && bytes[offset] != b'%' {263			offset += 1;264		}265		if offset != 0 {266			out.push(Element::String(&str[0..offset]));267		}268		if offset == bytes.len() {269			return Ok(out);270		}271		str = &str[offset + 1..];272		let (code, nstr) = parse_code(str)?;273		str = nstr;274		bytes = str.as_bytes();275		offset = 0;276277		out.push(Element::Code(code))278	}279}280281const NUMBERS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";282283#[inline]284pub fn render_integer(285	out: &mut String,286	iv: i64,287	padding: usize,288	precision: usize,289	blank: bool,290	sign: bool,291	radix: i64,292	prefix: &str,293	caps: bool,294) {295	// Digit char indexes in reverse order, i.e296	// for radix = 16 and n = 12f: [15, 2, 1]297	let digits = if iv == 0 {298		vec![0u8]299	} else {300		let mut v = iv.abs();301		let mut nums = Vec::with_capacity(1);302		while v > 0 {303			nums.push((v % radix) as u8);304			v /= radix;305		}306		nums307	};308	let neg = iv < 0;309	let zp = padding.saturating_sub(if neg || blank || sign { 1 } else { 0 });310	let zp2 = zp311		.max(precision)312		.saturating_sub(prefix.len() + digits.len());313314	if neg {315		out.push('-')316	} else if sign {317		out.push('+');318	} else if blank {319		out.push(' ');320	}321322	out.reserve(zp2);323	for _ in 0..zp2 {324		out.push('0');325	}326	out.push_str(prefix);327328	for digit in digits.into_iter().rev() {329		let ch = NUMBERS[digit as usize] as char;330		out.push(if caps { ch.to_ascii_uppercase() } else { ch });331	}332}333334pub fn render_decimal(335	out: &mut String,336	iv: i64,337	padding: usize,338	precision: usize,339	blank: bool,340	sign: bool,341) {342	render_integer(out, iv, padding, precision, blank, sign, 10, "", false)343}344pub fn render_octal(345	out: &mut String,346	iv: i64,347	padding: usize,348	precision: usize,349	alt: bool,350	blank: bool,351	sign: bool,352) {353	render_integer(354		out,355		iv,356		padding,357		precision,358		blank,359		sign,360		8,361		if alt && iv != 0 { "0" } else { "" },362		false,363	)364}365pub fn render_hexadecimal(366	out: &mut String,367	iv: i64,368	padding: usize,369	precision: usize,370	alt: bool,371	blank: bool,372	sign: bool,373	caps: bool,374) {375	render_integer(376		out,377		iv,378		padding,379		precision,380		blank,381		sign,382		16,383		match (alt, caps) {384			(true, true) => "0X",385			(true, false) => "0x",386			(false, _) => "",387		},388		caps,389	)390}391392pub fn render_float(393	out: &mut String,394	n: f64,395	mut padding: usize,396	precision: usize,397	blank: bool,398	sign: bool,399	ensure_pt: bool,400	trailing: bool,401) {402	let dot_size = if precision == 0 && !ensure_pt { 0 } else { 1 };403	padding = padding.saturating_sub(dot_size + precision);404	render_decimal(out, n.floor() as i64, padding, 0, blank, sign);405	if precision == 0 {406		if ensure_pt {407			out.push('.');408		}409		return;410	}411	let frac = n412		.fract()413		.mul_add(10.0_f64.powf(precision as f64), 0.5)414		.floor();415	if trailing || frac > 0.0 {416		out.push('.');417		let mut frac_str = String::new();418		render_decimal(&mut frac_str, frac as i64, precision, 0, false, false);419		let mut trim = frac_str.len();420		if !trailing {421			for b in frac_str.as_bytes().iter().rev() {422				if *b == b'0' {423					trim -= 1;424				}425			}426		}427		out.push_str(&frac_str[..trim]);428	} else if ensure_pt {429		out.push('.');430	}431}432433pub fn render_float_sci(434	out: &mut String,435	n: f64,436	mut padding: usize,437	precision: usize,438	blank: bool,439	sign: bool,440	ensure_pt: bool,441	trailing: bool,442	caps: bool,443) {444	let exponent = n.log10().floor();445	let mantissa = if exponent as i16 == -324 {446		n * 10.0 / 10.0_f64.powf(exponent + 1.0)447	} else {448		n / 10.0_f64.powf(exponent)449	};450	let mut exponent_str = String::new();451	render_decimal(&mut exponent_str, exponent as i64, 3, 0, false, true);452453	// +1 for e454	padding = padding.saturating_sub(exponent_str.len() + 1);455456	render_float(457		out, mantissa, padding, precision, blank, sign, ensure_pt, trailing,458	);459	out.push(if caps { 'E' } else { 'e' });460	out.push_str(&exponent_str);461}462463pub fn format_code(464	out: &mut String,465	value: &Val,466	code: &Code,467	width: usize,468	precision: Option<usize>,469) -> Result<()> {470	let clfags = &code.cflags;471	let (fpprec, iprec) = match precision {472		Some(v) => (v, v),473		None => (6, 0),474	};475	let padding = if clfags.zero && !clfags.left {476		width477	} else {478		0479	};480481	// TODO: If left padded, can optimize by writing directly to out482	let mut tmp_out = String::new();483484	match code.convtype {485		ConvTypeV::String => tmp_out.push_str(&value.clone().to_string()?),486		ConvTypeV::Decimal => {487			let value = value.clone().try_cast_num("%d/%u/%i requires number")?;488			render_decimal(489				&mut tmp_out,490				value as i64,491				padding,492				iprec,493				clfags.blank,494				clfags.sign,495			);496		}497		ConvTypeV::Octal => {498			let value = value.clone().try_cast_num("%o requires number")?;499			render_octal(500				&mut tmp_out,501				value as i64,502				padding,503				iprec,504				clfags.alt,505				clfags.blank,506				clfags.sign,507			);508		}509		ConvTypeV::Hexadecimal => {510			let value = value.clone().try_cast_num("%x/%X requires number")?;511			render_hexadecimal(512				&mut tmp_out,513				value as i64,514				padding,515				iprec,516				clfags.alt,517				clfags.blank,518				clfags.sign,519				code.caps,520			);521		}522		ConvTypeV::Scientific => {523			let value = value.clone().try_cast_num("%e/%E requires number")?;524			render_float_sci(525				&mut tmp_out,526				value,527				padding,528				fpprec,529				clfags.blank,530				clfags.sign,531				clfags.alt,532				true,533				code.caps,534			);535		}536		ConvTypeV::Float => {537			let value = value.clone().try_cast_num("%e/%E requires number")?;538			render_float(539				&mut tmp_out,540				value,541				padding,542				fpprec,543				clfags.blank,544				clfags.sign,545				clfags.alt,546				true,547			);548		}549		ConvTypeV::Shorter => {550			let value = value.clone().try_cast_num("%g/%G requires number")?;551			let exponent = value.log10().floor();552			if exponent < -4.0 || exponent >= fpprec as f64 {553				render_float_sci(554					&mut tmp_out,555					value,556					padding,557					fpprec - 1,558					clfags.blank,559					clfags.sign,560					clfags.alt,561					clfags.alt,562					code.caps,563				);564			} else {565				let digits_before_pt = 1.max(exponent as usize + 1);566				render_float(567					&mut tmp_out,568					value,569					padding,570					fpprec - digits_before_pt,571					clfags.blank,572					clfags.sign,573					clfags.alt,574					clfags.alt,575				);576			}577		}578		ConvTypeV::Char => match value.clone() {579			Val::Num(n) => tmp_out.push(580				std::char::from_u32(n as u32)581					.ok_or_else(|| InvalidUnicodeCodepointGot(n as u32))?,582			),583			Val::Str(s) => {584				if s.chars().count() != 1 {585					throw!(RuntimeError(586						format!("%c expected 1 char string, got {}", s.chars().count()).into(),587					));588				}589				tmp_out.push_str(&s);590			}591			_ => {592				throw!(TypeMismatch(593					"%c requires number/string",594					vec![ValType::Num, ValType::Str],595					value.value_type(),596				));597			}598		},599		ConvTypeV::Percent => tmp_out.push('%'),600	};601602	let padding = width.saturating_sub(tmp_out.len());603604	if !clfags.left {605		for _ in 0..padding {606			out.push(' ');607		}608	}609	out.push_str(&tmp_out);610	if clfags.left {611		for _ in 0..padding {612			out.push(' ');613		}614	}615616	Ok(())617}618619pub fn format_arr(str: &str, mut values: &[Val]) -> Result<String> {620	let codes = parse_codes(str)?;621	let mut out = String::new();622623	for code in codes {624		match code {625			Element::String(s) => {626				out.push_str(s);627			}628			Element::Code(c) => {629				let width = match c.width {630					Width::Star => {631						if values.is_empty() {632							throw!(NotEnoughValues);633						}634						let value = &values[0];635						values = &values[1..];636						value.clone().try_cast_num("field width")? as usize637					}638					Width::Fixed(n) => n,639				};640				let precision = match c.precision {641					Some(Width::Star) => {642						if values.is_empty() {643							throw!(NotEnoughValues);644						}645						let value = &values[0];646						values = &values[1..];647						Some(value.clone().try_cast_num("field precision")? as usize)648					}649					Some(Width::Fixed(n)) => Some(n),650					None => None,651				};652653				// %% should not consume a value654				let value = if c.convtype == ConvTypeV::Percent {655					&Val::Null656				} else {657					if values.is_empty() {658						throw!(NotEnoughValues);659					}660					let value = &values[0];661					values = &values[1..];662					value663				};664665				format_code(&mut out, value, &c, width, precision)?;666			}667		}668	}669670	Ok(out)671}672673pub fn format_obj(str: &str, values: &ObjValue) -> Result<String> {674	let codes = parse_codes(str)?;675	let mut out = String::new();676677	for code in codes {678		match code {679			Element::String(s) => {680				out.push_str(s);681			}682			Element::Code(c) => {683				// TODO: Operate on ref684				let f: IStr = c.mkey.into();685				let width = match c.width {686					Width::Star => {687						throw!(CannotUseStarWidthWithObject);688					}689					Width::Fixed(n) => n,690				};691				let precision = match c.precision {692					Some(Width::Star) => {693						throw!(CannotUseStarWidthWithObject);694					}695					Some(Width::Fixed(n)) => Some(n),696					None => None,697				};698699				let value = if c.convtype == ConvTypeV::Percent {700					Val::Null701				} else {702					if f.is_empty() {703						throw!(MappingKeysRequired);704					}705					if let Some(v) = values.get(f.clone())? {706						v707					} else {708						throw!(NoSuchFormatField(f));709					}710				};711712				format_code(&mut out, &value, &c, width, precision)?;713			}714		}715	}716717	Ok(out)718}719720#[cfg(test)]721pub mod test_format {722	use super::*;723724	#[test]725	fn parse() {726		assert_eq!(727			parse_codes(728				"How much error budget is left looking at our %.3f%% availability gurantees?"729			)730			.unwrap()731			.len(),732			4733		);734	}735736	#[test]737	fn octals() {738		assert_eq!(format_arr("%#o", &[Val::Num(8.0)]).unwrap(), "010");739		assert_eq!(format_arr("%#4o", &[Val::Num(8.0)]).unwrap(), " 010");740		assert_eq!(format_arr("%4o", &[Val::Num(8.0)]).unwrap(), "  10");741		assert_eq!(format_arr("%04o", &[Val::Num(8.0)]).unwrap(), "0010");742		assert_eq!(format_arr("%+4o", &[Val::Num(8.0)]).unwrap(), " +10");743		assert_eq!(format_arr("%+04o", &[Val::Num(8.0)]).unwrap(), "+010");744		assert_eq!(format_arr("%-4o", &[Val::Num(8.0)]).unwrap(), "10  ");745		assert_eq!(format_arr("%+-4o", &[Val::Num(8.0)]).unwrap(), "+10 ");746		assert_eq!(format_arr("%+-04o", &[Val::Num(8.0)]).unwrap(), "+10 ");747	}748749	#[test]750	fn percent_doesnt_consumes_values() {751		assert_eq!(752			format_arr(753				"How much error budget is left looking at our %.3f%% availability gurantees?",754				&[Val::Num(4.0)]755			)756			.unwrap(),757			"How much error budget is left looking at our 4.000% availability gurantees?"758		);759	}760}