use regex::Regex;
use std::cmp::{max, min};
use std::sync::LazyLock;
static FIELD_RANGE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(?P<left>-?\d+)?(?P<sep>\.\.)?(?P<right>-?\d+)?$").unwrap());
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum FieldRange {
Single(i32),
LeftInf(i32),
RightInf(i32),
Both(i32, i32),
}
impl FieldRange {
#[allow(clippy::should_implement_trait)]
pub fn from_str(range: &str) -> Option<FieldRange> {
use self::FieldRange::{Both, LeftInf, RightInf, Single};
let opt_caps = FIELD_RANGE.captures(range);
if let Some(caps) = opt_caps {
let opt_left = caps.name("left").map(|s| s.as_str().parse().unwrap_or(1));
let opt_right = caps.name("right").map(|s| s.as_str().parse().unwrap_or(-1));
let opt_sep = caps.name("sep").map(|s| s.as_str().to_string());
match (opt_left, opt_right) {
(None, None) => Some(RightInf(0)),
(Some(left), None) => {
match opt_sep {
None => Some(Single(left)), Some(_) => Some(RightInf(left)), }
}
(None, Some(right)) => {
match opt_sep {
None => Some(Single(right)), Some(_) => Some(LeftInf(right)), }
}
(Some(left), Some(right)) => Some(Both(left, right)), }
} else {
None
}
}
#[must_use]
pub fn to_index_pair(&self, length: usize) -> Option<(usize, usize)> {
use self::FieldRange::{Both, LeftInf, RightInf, Single};
match *self {
Single(num) => {
let num = FieldRange::translate_neg(num, length);
if num == 0 || num > length {
None
} else {
Some((num - 1, num))
}
}
LeftInf(right) => {
let right = FieldRange::translate_neg(right, length);
if length == 0 || right == 0 {
None
} else {
let right = min(right, length);
Some((0, right))
}
}
RightInf(left) => {
let left = FieldRange::translate_neg(left, length);
if length == 0 || left > length {
None
} else {
let left = max(left, 1);
Some((left - 1, length))
}
}
Both(left, right) => {
let left = FieldRange::translate_neg(left, length);
let right = FieldRange::translate_neg(right, length);
if length == 0 || right == 0 || left > right || left > length {
None
} else {
Some((max(left, 1) - 1, min(right, length)))
}
}
}
}
fn translate_neg(idx: i32, length: usize) -> usize {
let len = i32::try_from(length).unwrap_or(i32::MAX);
let idx = if idx < 0 { idx + len + 1 } else { idx };
max(0, idx).unsigned_abs() as usize
}
}
fn get_ranges_by_delimiter(delimiter: &Regex, text: &str) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let mut last = 0;
for mat in delimiter.find_iter(text) {
ranges.push((last, mat.start()));
last = mat.end();
}
ranges.push((last, text.len()));
ranges
}
#[must_use]
pub fn get_string_by_field<'a>(delimiter: &Regex, text: &'a str, field: &FieldRange) -> Option<&'a str> {
let ranges = get_ranges_by_delimiter(delimiter, text);
if let Some((start, stop)) = field.to_index_pair(ranges.len()) {
let &(begin, _) = &ranges[start];
let &(_, end) = ranges.get(stop - 1).unwrap_or(&(text.len(), 0));
Some(&text[begin..end])
} else {
None
}
}
#[must_use]
pub fn get_string_by_range<'a>(delimiter: &Regex, text: &'a str, range: &str) -> Option<&'a str> {
FieldRange::from_str(range).and_then(|field| get_string_by_field(delimiter, text, &field))
}
#[must_use]
pub fn parse_matching_fields(delimiter: &Regex, text: &str, fields: &[FieldRange]) -> Vec<(usize, usize)> {
let ranges = get_ranges_by_delimiter(delimiter, text);
let mut ret = Vec::new();
for field in fields {
if let Some((start, stop)) = field.to_index_pair(ranges.len()) {
let &(begin, _) = &ranges[start];
let &(end, _) = ranges.get(stop).unwrap_or(&(text.len(), 0));
ret.push((begin, end));
}
}
ret
}
#[must_use]
pub fn parse_transform_fields(delimiter: &Regex, text: &str, fields: &[FieldRange]) -> String {
let ranges = get_ranges_by_delimiter(delimiter, text);
let mut ret = String::new();
for field in fields {
if let Some((start, stop)) = field.to_index_pair(ranges.len()) {
let &(begin, _) = &ranges[start];
let &(end, _) = ranges.get(stop).unwrap_or(&(text.len(), 0));
ret.push_str(&text[begin..end]);
}
}
ret
}
#[cfg(test)]
#[path = "field_tests.rs"]
mod test;