git.delta.rocks / jrsonnet / refs/heads / master

difftreelog

source

crates/jrsonnet-stdlib/src/strings.rs7.0 KiBsourcehistory
1use std::collections::BTreeSet;23use jrsonnet_evaluator::{4	Either, IStr, Val, bail,5	error::{ErrorKind::*, Result},6	function::builtin,7	typed::{Either2, FromUntyped, M1},8	val::{ArrValue, IndexableVal},9};1011#[builtin]12pub const fn builtin_codepoint(str: char) -> u32 {13	str as u3214}1516#[builtin]17pub fn builtin_substr(str: IStr, from: usize, len: usize) -> String {18	str.chars().skip(from).take(len).collect()19}2021#[builtin]22pub fn builtin_char(n: u32) -> Result<char> {23	Ok(std::char::from_u32(n).ok_or_else(|| InvalidUnicodeCodepointGot(n))?)24}2526#[builtin]27pub fn builtin_str_replace(str: String, from: IStr, to: IStr) -> Result<String> {28	if from.is_empty() {29		bail!("`from` string must not be zero length");30	}31	Ok(str.replace(&from as &str, &to as &str))32}3334#[builtin]35pub fn builtin_escape_string_bash(str_: String) -> String {36	const QUOTE: char = '\'';37	let mut out = str_.replace(QUOTE, "'\"'\"'");38	out.insert(0, QUOTE);39	out.push(QUOTE);40	out41}4243#[builtin]44pub fn builtin_escape_string_dollars(str_: String) -> String {45	str_.replace('$', "$$")46}4748#[builtin]49pub fn builtin_is_empty(str: String) -> bool {50	str.is_empty()51}5253#[builtin]54pub fn builtin_equals_ignore_case(str1: String, str2: String) -> bool {55	str1.eq_ignore_ascii_case(&str2)56}5758#[builtin]59pub fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {60	use Either2::*;61	match maxsplits {62		A(n) => str.splitn(n + 1, &c as &str).map(Val::string).collect(),63		B(_) => str.split(&c as &str).map(Val::string).collect(),64	}65}6667#[builtin]68pub fn builtin_splitlimitr(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {69	use Either2::*;70	match maxsplits {71		A(n) =>72		// rsplitn does not implement DoubleEndedIterator so collect into73		// a temporary vec74		{75			str.rsplitn(n + 1, &c as &str)76				.map(Val::string)77				.collect::<Vec<_>>()78				.into_iter()79				.rev()80				.collect()81		}82		B(_) => str.split(&c as &str).map(Val::string).collect(),83	}84}8586#[builtin]87pub fn builtin_split(str: IStr, c: IStr) -> ArrValue {88	use Either2::*;89	builtin_splitlimit(str, c, B(M1))90}9192#[builtin]93pub fn builtin_ascii_upper(str: IStr) -> String {94	str.to_ascii_uppercase()95}9697#[builtin]98pub fn builtin_ascii_lower(str: IStr) -> String {99	str.to_ascii_lowercase()100}101102#[builtin]103pub fn builtin_find_substr(pat: IStr, str: IStr) -> ArrValue {104	if pat.is_empty() || str.is_empty() || pat.len() > str.len() {105		return ArrValue::empty();106	}107108	let str = str.as_str();109	let pat = pat.as_bytes();110	let strb = str.as_bytes();111112	let max_pos = str.len() - pat.len();113114	let mut out: Vec<Val> = Vec::new();115	for (ch_idx, (i, _)) in str116		.char_indices()117		.take_while(|(i, _)| i <= &max_pos)118		.enumerate()119	{120		if &strb[i..i + pat.len()] == pat {121			out.push(Val::Num(122				ch_idx.try_into().expect("unrealisticly long string"),123			));124		}125	}126	out.into()127}128129#[builtin]130pub fn builtin_parse_int(str: IStr) -> Result<f64> {131	if let Some(raw) = str.strip_prefix('-') {132		if raw.is_empty() {133			bail!("integer only consists of a minus")134		}135136		parse_nat::<10>(raw).map(|value| -value)137	} else {138		if str.is_empty() {139			bail!("empty integer")140		}141142		parse_nat::<10>(str.as_str())143	}144}145146#[builtin]147pub fn builtin_parse_octal(str: IStr) -> Result<f64> {148	if str.is_empty() {149		bail!("empty octal integer");150	}151152	parse_nat::<8>(str.as_str())153}154155#[builtin]156pub fn builtin_parse_hex(str: IStr) -> Result<f64> {157	if str.is_empty() {158		bail!("empty hexadecimal integer");159	}160161	parse_nat::<16>(str.as_str())162}163164fn parse_nat<const BASE: u32>(raw: &str) -> Result<f64> {165	const ZERO_CODE: u32 = '0' as u32;166	const UPPER_A_CODE: u32 = 'A' as u32;167	const LOWER_A_CODE: u32 = 'a' as u32;168169	#[inline]170	fn checked_sub_if(condition: bool, lhs: u32, rhs: u32) -> Option<u32> {171		if condition {172			lhs.checked_sub(rhs)173		} else {174			None175		}176	}177178	debug_assert!(179		1 <= BASE && BASE <= 16,180		"integer base should be between 1 and 16"181	);182183	let base = f64::from(BASE);184185	raw.chars().try_fold(0f64, |aggregate, digit| {186		let digit = digit as u32;187		// if-let-else looks better here than Option combinators188		#[allow(clippy::option_if_let_else)]189		let digit = if let Some(digit) = checked_sub_if(BASE > 10, digit, LOWER_A_CODE) {190			digit + 10191		} else if let Some(digit) = checked_sub_if(BASE > 10, digit, UPPER_A_CODE) {192			digit + 10193		} else {194			digit.checked_sub(ZERO_CODE).unwrap_or(BASE)195		};196197		if digit < BASE {198			Ok(base.mul_add(aggregate, f64::from(digit)))199		} else {200			bail!("{raw:?} is not a base {BASE} integer");201		}202	})203}204205#[cfg(feature = "exp-bigint")]206#[builtin]207pub fn builtin_bigint(v: Either![f64, IStr]) -> Result<Val> {208	use Either2::*;209	use jrsonnet_evaluator::error;210	Ok(match v {211		A(a) => Val::BigInt(Box::new(212			a.to_string()213				.parse()214				.map_err(|e| error!("number is not convertible to bigint: {e}"))?,215		)),216		B(b) => Val::BigInt(Box::new(217			b.as_str().parse().map_err(|e| error!("bad bigint: {e}"))?,218		)),219	})220}221222#[builtin]223pub fn builtin_string_chars(str: IStr) -> ArrValue {224	str.chars().collect()225}226227#[builtin]228pub fn builtin_lstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {229	if str.is_empty() || chars.is_empty() {230		return Ok(str);231	}232233	let pattern = new_trim_pattern(chars)?;234	Ok(str.as_str().trim_start_matches(pattern).into())235}236237#[builtin]238pub fn builtin_rstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {239	if str.is_empty() || chars.is_empty() {240		return Ok(str);241	}242243	let pattern = new_trim_pattern(chars)?;244	Ok(str.as_str().trim_end_matches(pattern).into())245}246247#[builtin]248pub fn builtin_strip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {249	if str.is_empty() || chars.is_empty() {250		return Ok(str);251	}252253	let pattern = new_trim_pattern(chars)?;254	Ok(str.as_str().trim_matches(pattern).into())255}256257#[builtin]258pub fn builtin_trim(str: IStr) -> String {259	let filter =260		|v: char| {261			v == ' '262				|| v == '\t' || v == '\n'263				|| v == '\u{000c}'264				|| v == '\r' || v == '\u{0085}'265				|| v == '\u{00a0}'266		};267	str.as_str().trim_matches(filter).to_string()268}269270fn new_trim_pattern(chars: IndexableVal) -> Result<impl Fn(char) -> bool> {271	let chars: BTreeSet<char> = match chars {272		IndexableVal::Str(chars) => chars.chars().collect(),273		IndexableVal::Arr(chars) => chars274			.iter()275			.filter_map(|it| it.map(|it| char::from_untyped(it).ok()).transpose())276			.collect::<Result<_, _>>()?,277	};278279	Ok(move |char| chars.contains(&char))280}281282#[cfg(test)]283#[allow(clippy::float_cmp)]284mod tests {285	use super::*;286287	#[test]288	fn parse_nat_base_8() {289		assert_eq!(parse_nat::<8>("0").unwrap(), 0.);290		assert_eq!(parse_nat::<8>("5").unwrap(), 5.);291		assert_eq!(parse_nat::<8>("32").unwrap(), f64::from(0o32));292		assert_eq!(parse_nat::<8>("761").unwrap(), f64::from(0o761));293	}294295	#[test]296	fn parse_nat_base_10() {297		assert_eq!(parse_nat::<10>("0").unwrap(), 0.);298		assert_eq!(parse_nat::<10>("3").unwrap(), 3.);299		assert_eq!(parse_nat::<10>("27").unwrap(), 27.);300		assert_eq!(parse_nat::<10>("123").unwrap(), 123.);301	}302303	#[test]304	fn parse_nat_base_16() {305		assert_eq!(parse_nat::<16>("0").unwrap(), 0.);306		assert_eq!(parse_nat::<16>("A").unwrap(), 10.);307		assert_eq!(parse_nat::<16>("a9").unwrap(), f64::from(0xA9));308		assert_eq!(parse_nat::<16>("BbC").unwrap(), f64::from(0xBBC));309	}310}