use std::sync::Arc;
pub trait PositionMapper: Send + Sync {
fn raw_to_formatted(&self, raw: &str, formatted: &str, raw_pos: usize) -> usize;
fn formatted_to_raw(&self, raw: &str, formatted: &str, formatted_pos: usize) -> usize;
}
#[derive(Clone, Default)]
pub struct DefaultPositionMapper;
impl PositionMapper for DefaultPositionMapper {
fn raw_to_formatted(&self, raw: &str, formatted: &str, raw_pos: usize) -> usize {
let raw_len = raw.chars().count();
let clamped_raw_pos = raw_pos.min(raw_len);
let mut seen_user_chars = 0usize;
for (idx, ch) in formatted.char_indices() {
if ch.is_alphanumeric() {
if seen_user_chars == clamped_raw_pos {
return idx;
}
seen_user_chars += 1;
}
}
formatted.len()
}
fn formatted_to_raw(&self, raw: &str, formatted: &str, formatted_pos: usize) -> usize {
let clamped_fmt_pos = formatted_pos.min(formatted.len());
let mut seen_user_chars = 0usize;
for (idx, ch) in formatted.char_indices() {
if idx >= clamped_fmt_pos {
break;
}
if ch.is_alphanumeric() {
seen_user_chars += 1;
}
}
let raw_len = raw.chars().count();
seen_user_chars.min(raw_len)
}
}
pub enum FormattingResult {
Success {
formatted: String,
mapper: Arc<dyn PositionMapper>,
},
Warning {
formatted: String,
message: String,
mapper: Arc<dyn PositionMapper>,
},
Error { message: String },
}
impl FormattingResult {
pub fn success(formatted: impl Into<String>) -> Self {
FormattingResult::Success {
formatted: formatted.into(),
mapper: Arc::new(DefaultPositionMapper),
}
}
pub fn warning(formatted: impl Into<String>, message: impl Into<String>) -> Self {
FormattingResult::Warning {
formatted: formatted.into(),
message: message.into(),
mapper: Arc::new(DefaultPositionMapper),
}
}
pub fn success_with_mapper(
formatted: impl Into<String>,
mapper: Arc<dyn PositionMapper>,
) -> Self {
FormattingResult::Success {
formatted: formatted.into(),
mapper,
}
}
pub fn warning_with_mapper(
formatted: impl Into<String>,
message: impl Into<String>,
mapper: Arc<dyn PositionMapper>,
) -> Self {
FormattingResult::Warning {
formatted: formatted.into(),
message: message.into(),
mapper,
}
}
pub fn error(message: impl Into<String>) -> Self {
FormattingResult::Error {
message: message.into(),
}
}
}
pub trait CustomFormatter: Send + Sync {
fn format(&self, raw: &str) -> FormattingResult;
}
#[cfg(test)]
mod tests {
use super::*;
struct GroupEvery3;
impl CustomFormatter for GroupEvery3 {
fn format(&self, raw: &str) -> FormattingResult {
let mut out = String::new();
for (i, ch) in raw.chars().enumerate() {
if i > 0 && i % 3 == 0 {
out.push(' ');
}
out.push(ch);
}
FormattingResult::success(out)
}
}
#[test]
fn default_mapper_roundtrip_basic() {
let mapper = DefaultPositionMapper;
let raw = "01001";
let formatted = "010 01";
for rp in 0..=raw.chars().count() {
let fp = mapper.raw_to_formatted(raw, formatted, rp);
assert!(fp <= formatted.len());
}
for fp in 0..=formatted.len() {
let rp = mapper.formatted_to_raw(raw, formatted, fp);
assert!(rp <= raw.chars().count());
}
}
#[test]
fn formatter_groups_every_3() {
let f = GroupEvery3;
match f.format("1234567") {
FormattingResult::Success { formatted, .. } => {
assert_eq!(formatted, "123 456 7");
}
_ => panic!("expected success"),
}
}
}