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

difftreelog

source

crates/jrsonnet-stdlib/src/strings.rs7.1 KiBsourcehistory
1use std::collections::BTreeSet;23use jrsonnet_evaluator::{4	bail,5	error::{ErrorKind::*, Result},6	function::builtin,7	typed::{Either2, Typed, M1},8	val::{ArrValue, IndexableVal},9	Either, IStr, Val,10};1112#[builtin]13pub const fn builtin_codepoint(str: char) -> u32 {14	str as u3215}1617#[builtin]18pub fn builtin_substr(str: IStr, from: usize, len: usize) -> String {19	str.chars().skip(from).take(len).collect()20}2122#[builtin]23pub fn builtin_char(n: u32) -> Result<char> {24	Ok(std::char::from_u32(n).ok_or_else(|| InvalidUnicodeCodepointGot(n))?)25}2627#[builtin]28pub fn builtin_str_replace(str: String, from: IStr, to: IStr) -> Result<String> {29	if from.is_empty() {30		bail!("'from' string must not be zero length");31	}32	Ok(str.replace(&from as &str, &to as &str))33}3435#[builtin]36pub fn builtin_escape_string_bash(str_: String) -> String {37	const QUOTE: char = '\'';38	let mut out = str_.replace(QUOTE, "'\"'\"'");39	out.insert(0, QUOTE);40	out.push(QUOTE);41	out42}4344#[builtin]45pub fn builtin_escape_string_dollars(str_: String) -> String {46	str_.replace('$', "$$")47}4849#[builtin]50pub fn builtin_is_empty(str: String) -> bool {51	str.is_empty()52}5354#[builtin]55pub fn builtin_equals_ignore_case(str1: String, str2: String) -> bool {56	str1.eq_ignore_ascii_case(&str2)57}5859#[builtin]60pub fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {61	use Either2::*;62	match maxsplits {63		A(n) => str.splitn(n + 1, &c as &str).map(Val::string).collect(),64		B(_) => str.split(&c as &str).map(Val::string).collect(),65	}66}6768#[builtin]69pub fn builtin_splitlimitr(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {70	use Either2::*;71	match maxsplits {72		A(n) =>73		// rsplitn does not implement DoubleEndedIterator so collect into74		// a temporary vec75		{76			str.rsplitn(n + 1, &c as &str)77				.map(Val::string)78				.collect::<Vec<_>>()79				.into_iter()80				.rev()81				.collect()82		}83		B(_) => str.split(&c as &str).map(Val::string).collect(),84	}85}8687#[builtin]88pub fn builtin_split(str: IStr, c: IStr) -> ArrValue {89	use Either2::*;90	builtin_splitlimit(str, c, B(M1))91}9293#[builtin]94pub fn builtin_ascii_upper(str: IStr) -> String {95	str.to_ascii_uppercase()96}9798#[builtin]99pub fn builtin_ascii_lower(str: IStr) -> String {100	str.to_ascii_lowercase()101}102103#[builtin]104pub fn builtin_find_substr(pat: IStr, str: IStr) -> ArrValue {105	if pat.is_empty() || str.is_empty() || pat.len() > str.len() {106		return ArrValue::empty();107	}108109	let str = str.as_str();110	let pat = pat.as_bytes();111	let strb = str.as_bytes();112113	let max_pos = str.len() - pat.len();114115	let mut out: Vec<Val> = Vec::new();116	for (ch_idx, (i, _)) in str117		.char_indices()118		.take_while(|(i, _)| i <= &max_pos)119		.enumerate()120	{121		if &strb[i..i + pat.len()] == pat {122			out.push(Val::Num(123				ch_idx.try_into().expect("unrealisticly long string"),124			));125		}126	}127	out.into()128}129130#[builtin]131pub fn builtin_parse_int(str: IStr) -> Result<f64> {132	if let Some(raw) = str.strip_prefix('-') {133		if raw.is_empty() {134			bail!("integer only consists of a minus")135		}136137		parse_nat::<10>(raw).map(|value| -value)138	} else {139		if str.is_empty() {140			bail!("empty integer")141		}142143		parse_nat::<10>(str.as_str())144	}145}146147#[builtin]148pub fn builtin_parse_octal(str: IStr) -> Result<f64> {149	if str.is_empty() {150		bail!("empty octal integer");151	}152153	parse_nat::<8>(str.as_str())154}155156#[builtin]157pub fn builtin_parse_hex(str: IStr) -> Result<f64> {158	if str.is_empty() {159		bail!("empty hexadecimal integer");160	}161162	parse_nat::<16>(str.as_str())163}164165fn parse_nat<const BASE: u32>(raw: &str) -> Result<f64> {166	const ZERO_CODE: u32 = '0' as u32;167	const UPPER_A_CODE: u32 = 'A' as u32;168	const LOWER_A_CODE: u32 = 'a' as u32;169170	#[inline]171	fn checked_sub_if(condition: bool, lhs: u32, rhs: u32) -> Option<u32> {172		if condition {173			lhs.checked_sub(rhs)174		} else {175			None176		}177	}178179	debug_assert!(180		1 <= BASE && BASE <= 16,181		"integer base should be between 1 and 16"182	);183184	let base = f64::from(BASE);185186	raw.chars().try_fold(0f64, |aggregate, digit| {187		let digit = digit as u32;188		// if-let-else looks better here than Option combinators189		#[allow(clippy::option_if_let_else)]190		let digit = if let Some(digit) = checked_sub_if(BASE > 10, digit, LOWER_A_CODE) {191			digit + 10192		} else if let Some(digit) = checked_sub_if(BASE > 10, digit, UPPER_A_CODE) {193			digit + 10194		} else {195			digit.checked_sub(ZERO_CODE).unwrap_or(BASE)196		};197198		if digit < BASE {199			Ok(base.mul_add(aggregate, f64::from(digit)))200		} else {201			bail!("{raw:?} is not a base {BASE} integer");202		}203	})204}205206#[cfg(feature = "exp-bigint")]207#[builtin]208pub fn builtin_bigint(v: Either![f64, IStr]) -> Result<Val> {209	use jrsonnet_evaluator::runtime_error;210	use Either2::*;211	Ok(match v {212		A(a) => {213			Val::BigInt(Box::new(a.to_string().parse().map_err(|e| {214				runtime_error!("number is not convertible to bigint: {e}")215			})?))216		}217		B(b) => Val::BigInt(Box::new(218			b.as_str()219				.parse()220				.map_err(|e| runtime_error!("bad bigint: {e}"))?,221		)),222	})223}224225#[builtin]226pub fn builtin_string_chars(str: IStr) -> ArrValue {227	ArrValue::chars(str.chars())228}229230#[builtin]231pub fn builtin_lstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {232	if str.is_empty() || chars.is_empty() {233		return Ok(str);234	}235236	let pattern = new_trim_pattern(chars)?;237	Ok(str.as_str().trim_start_matches(pattern).into())238}239240#[builtin]241pub fn builtin_rstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {242	if str.is_empty() || chars.is_empty() {243		return Ok(str);244	}245246	let pattern = new_trim_pattern(chars)?;247	Ok(str.as_str().trim_end_matches(pattern).into())248}249250#[builtin]251pub fn builtin_strip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {252	if str.is_empty() || chars.is_empty() {253		return Ok(str);254	}255256	let pattern = new_trim_pattern(chars)?;257	Ok(str.as_str().trim_matches(pattern).into())258}259260#[builtin]261pub fn builtin_trim(str: IStr) -> String {262	let filter =263		|v: char| {264			v == ' '265				|| v == '\t' || v == '\n'266				|| v == '\u{000c}'267				|| v == '\r' || v == '\u{0085}'268				|| v == '\u{00a0}'269		};270	str.as_str().trim_matches(filter).to_string()271}272273fn new_trim_pattern(chars: IndexableVal) -> Result<impl Fn(char) -> bool> {274	let chars: BTreeSet<char> = match chars {275		IndexableVal::Str(chars) => chars.chars().collect(),276		IndexableVal::Arr(chars) => chars277			.iter()278			.filter_map(|it| it.map(|it| char::from_untyped(it).ok()).transpose())279			.collect::<Result<_, _>>()?,280	};281282	Ok(move |char| chars.contains(&char))283}284285#[cfg(test)]286#[allow(clippy::float_cmp)]287mod tests {288	use super::*;289290	#[test]291	fn parse_nat_base_8() {292		assert_eq!(parse_nat::<8>("0").unwrap(), 0.);293		assert_eq!(parse_nat::<8>("5").unwrap(), 5.);294		assert_eq!(parse_nat::<8>("32").unwrap(), f64::from(0o32));295		assert_eq!(parse_nat::<8>("761").unwrap(), f64::from(0o761));296	}297298	#[test]299	fn parse_nat_base_10() {300		assert_eq!(parse_nat::<10>("0").unwrap(), 0.);301		assert_eq!(parse_nat::<10>("3").unwrap(), 3.);302		assert_eq!(parse_nat::<10>("27").unwrap(), 27.);303		assert_eq!(parse_nat::<10>("123").unwrap(), 123.);304	}305306	#[test]307	fn parse_nat_base_16() {308		assert_eq!(parse_nat::<16>("0").unwrap(), 0.);309		assert_eq!(parse_nat::<16>("A").unwrap(), 10.);310		assert_eq!(parse_nat::<16>("a9").unwrap(), f64::from(0xA9));311		assert_eq!(parse_nat::<16>("BbC").unwrap(), f64::from(0xBBC));312	}313}