use crate::ktlint::console::LINE_SEPARATOR;
const FLAGS: &str = "-#+ 0,(<";
const CONVERSIONS: &str = "bBhHsScCdoxXeEfgGaA%n";
const DATE_TIME_CONVERSIONS: &str = "HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc";
enum Part<'a> {
Fixed(&'a str),
Specifier(Specifier),
}
struct Specifier {
text: String,
flags: String,
width: Option<u32>,
precision: Option<u32>,
date_time: bool,
conversion: char,
}
pub fn java_printf(text: &str) -> Result<String, String> {
let mut out = String::new();
for part in parse(text)? {
match part {
Part::Fixed(s) => out.push_str(s),
Part::Specifier(s) if s.conversion == 'n' => out.push_str(LINE_SEPARATOR),
Part::Specifier(s) if s.conversion == '%' => {
let width = s.width.unwrap_or(0) as usize;
if s.flags.contains('-') { out.push_str(&format!("{:<width$}", "%")) } else { out.push_str(&format!("{:>width$}", "%")) }
}
Part::Specifier(s) => return Err(format!("java.util.MissingFormatArgumentException: Format specifier '{}'", s.text)),
}
}
Ok(out)
}
fn parse(text: &str) -> Result<Vec<Part<'_>>, String> {
let unknown = |conversion: &str| format!("java.util.UnknownFormatConversionException: Conversion = '{conversion}'");
let mut parts = Vec::new();
let mut rest = text;
while let Some(n) = rest.find('%') {
if n > 0 {
parts.push(Part::Fixed(&rest[..n]));
}
let after = &rest[n + 1..];
let first = after.chars().next().ok_or_else(|| unknown("%"))?;
let (specifier, length) = match parse_specifier(after) {
Some(parsed) => parsed,
None => return Err(unknown(&first.to_string())),
};
check(&specifier)?;
parts.push(Part::Specifier(specifier));
rest = &after[length..];
}
if !rest.is_empty() {
parts.push(Part::Fixed(rest));
}
Ok(parts)
}
fn parse_specifier(s: &str) -> Option<(Specifier, usize)> {
let bytes = s.as_bytes();
let digits = |from: usize| bytes[from..].iter().take_while(|b| b.is_ascii_digit()).count();
let mut i = 0;
let index_digits = digits(0);
if index_digits > 0 && bytes.get(index_digits) == Some(&b'$') {
i = index_digits + 1;
}
let flags_start = i;
while bytes.get(i).is_some_and(|b| FLAGS.as_bytes().contains(b)) {
i += 1;
}
let flags = s[flags_start..i].to_owned();
let width_digits = digits(i);
let width = (width_digits > 0).then(|| s[i..i + width_digits].parse().unwrap_or(u32::MAX));
i += width_digits;
let mut precision = None;
if bytes.get(i) == Some(&b'.') {
let precision_digits = digits(i + 1);
if precision_digits == 0 {
return None;
}
precision = Some(s[i + 1..i + 1 + precision_digits].parse().unwrap_or(u32::MAX));
i += 1 + precision_digits;
}
let date_time = matches!(bytes.get(i), Some(b't' | b'T'));
if date_time {
i += 1;
}
let conversion = s[i..].chars().next().filter(|c| c.is_ascii_alphabetic() || *c == '%')?;
i += 1;
let specifier = Specifier { text: format!("%{}", &s[..i]), flags, width, precision, date_time, conversion };
Some((specifier, i))
}
fn check(s: &Specifier) -> Result<(), String> {
let unknown = |conversion: String| Err(format!("java.util.UnknownFormatConversionException: Conversion = '{conversion}'"));
if s.date_time {
return if DATE_TIME_CONVERSIONS.contains(s.conversion) { Ok(()) } else { unknown(format!("t{}", s.conversion)) };
}
if !CONVERSIONS.contains(s.conversion) {
return unknown(s.conversion.to_string());
}
match s.conversion {
'n' if s.precision.is_some() => Err(format!("java.util.IllegalFormatPrecisionException: {}", s.precision.unwrap())),
'n' if s.width.is_some() => Err(format!("java.util.IllegalFormatWidthException: {}", s.width.unwrap())),
'n' if !s.flags.is_empty() => Err(format!("java.util.IllegalFormatFlagsException: Flags = '{}'", s.flags)),
'%' if s.precision.is_some() => Err(format!("java.util.IllegalFormatPrecisionException: {}", s.precision.unwrap())),
'%' if s.flags.chars().any(|c| c != '-') => Err(format!("java.util.IllegalFormatFlagsException: Flags = '{}'", s.flags)),
'%' if s.flags.contains('-') && s.width.is_none() => Err(format!("java.util.MissingFormatWidthException: {}", s.text)),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rewrites_text_conversions_and_rejects_the_rest() {
assert_eq!(java_printf("val a = \"x\"\n"), Ok("val a = \"x\"\n".to_owned()));
assert_eq!(java_printf("100%% %5%|%-3%|"), Ok("100% %|% |".to_owned()));
assert_eq!(java_printf("a%nb"), Ok(format!("a{LINE_SEPARATOR}b")));
assert_eq!(java_printf("\"%d %s\""), Err("java.util.MissingFormatArgumentException: Format specifier '%d'".to_owned()));
assert_eq!(java_printf("%-5.2f"), Err("java.util.MissingFormatArgumentException: Format specifier '%-5.2f'".to_owned()));
assert_eq!(java_printf("%1$s"), Err("java.util.MissingFormatArgumentException: Format specifier '%1$s'".to_owned()));
assert_eq!(java_printf("%d \"100%\""), Err("java.util.UnknownFormatConversionException: Conversion = '\"'".to_owned()));
assert_eq!(java_printf("50%"), Err("java.util.UnknownFormatConversionException: Conversion = '%'".to_owned()));
assert_eq!(java_printf("%q"), Err("java.util.UnknownFormatConversionException: Conversion = 'q'".to_owned()));
assert_eq!(java_printf("%tq"), Err("java.util.UnknownFormatConversionException: Conversion = 'tq'".to_owned()));
assert_eq!(java_printf("%tY"), Err("java.util.MissingFormatArgumentException: Format specifier '%tY'".to_owned()));
assert_eq!(java_printf("%-%"), Err("java.util.MissingFormatWidthException: %-%".to_owned()));
}
}