Crate streplace

Source
Expand description

§STReplace

crates.io docs

STReplace is a small library for matching and replacing in strings and slices with user-defined functions. It does not currently support overlapping matches.

It provides several extension methods to the String and &str types, for this purpose.

This crate also includes several feature flags:

  • result: Provides variants of get_matches, replace_matches, and match_and_replace which accept functions that return Result values.
  • convenience: Provides additional convenience Matcher and Replacer implementations, which can be used in place of bare functions.

§Benchmarks

On a very basic benchmark, based on the first example in the section below:

This benchmark is now several versions out of date, but should remain approximately accurate.

§Examples

use streplace::{InProgressMatch, Match, MatchResult, Matchable};

fn main() {
    let test = "hello test, this is a test string";
    let new = test.match_and_replace(
        |progress, index, character| match progress {
            Some(InProgressMatch { start, value }) => {
                if index - start >= 4 {
                    println!("end {start} {value} {character}");
                    MatchResult::End
                } else if "test"
                    .chars()
                    .nth(index - start)
                    .is_some_and(|c| character == c)
                {
                    println!("cont {start} {value}{character}");
                    MatchResult::Continue
                } else {
                    println!("fail {start} {value} {character}");
                    MatchResult::Fail
                }
            }
            None => {
                if character == 't' {
                    println!("start {index} {character}");
                    MatchResult::Start
                } else {
                    println!("none {index} {character}");
                    MatchResult::None
                }
            }
        },
        |Match { start, end, value }| format!("start:{start},end:{end},val:{value}"),
        false,
    );
    println!("{new}");
}
use streplace::{
    AccumulatorMatcher, ChainMatchable, InProgressMatch, Match, MatchResult, Matchable,
};

fn main() {
    let text = "@first_of(a b c)";
    let text = text.match_and_replace(
        "@first_of(".chain(AccumulatorMatcher::new(
            0,
            |progress, index, character, parens| {
                let value = progress.map(|p| p.value);
                let mut last_chars = value.unwrap_or_default().chars().rev();
                let last_char = last_chars.next();
                let second_last_char = last_chars.next();
                if character == '(' && last_char != Some('\\') {
                    *parens += 1;
                }
                if last_char.is_some_and(|c| c == ')') && second_last_char != Some('\\') {
                    println!("last char is paren");
                    if *parens == 0 {
                        *parens = 0;
                        MatchResult::End
                    } else {
                        *parens -= 1;
                        MatchResult::Continue
                    }
                } else if index == text.len() - 1 && !(character == ')' && last_char != Some('\\'))
                {
                    *parens = 0;
                    MatchResult::Fail
                } else {
                    MatchResult::Continue
                }
            },
        )),
        |Match {
             start: _,
             end: _,
             value,
         }| {
            let substring = &value["@first_of(".len()..value.len() - 1];

            let submatches: Vec<&str> = substring
                .get_matches(
                    AccumulatorMatcher::new(0, |progress, _, character, parens| {
                        match (progress, character) {
                            (Some(_), ' ') => {
                                if *parens == 0 {
                                    MatchResult::End
                                } else {
                                    MatchResult::Continue
                                }
                            }
                            (
                                Some(InProgressMatch {
                                    start: _,
                                    value: match_value,
                                }),
                                '(',
                            ) => {
                                if match_value.ends_with('\\') {
                                    MatchResult::Continue
                                } else {
                                    *parens += 1;
                                    MatchResult::Continue
                                }
                            }
                            (
                                Some(InProgressMatch {
                                    start: _,
                                    value: match_value,
                                }),
                                ')',
                            ) => {
                                if match_value.ends_with('\\') {
                                    MatchResult::Continue
                                } else {
                                    *parens = (*parens - 1).max(0);
                                    MatchResult::Continue
                                }
                            }
                            (Some(_), _) => MatchResult::Continue,
                            (None, ' ') => {
                                *parens = 0;
                                MatchResult::None
                            }
                            (None, _) => {
                                *parens = 0;
                                MatchResult::Start
                            }
                        }
                    }),
                    true,
                )
                .into_iter()
                .map(|m| m.value)
                .collect();

            let chosen = submatches
                .first()
                .map(|v| v.to_string())
                .unwrap_or_default();
            chosen
        },
        true,
    );

    println!("{text}");
}

§Changelog

  • 3.0.0
    • Replaced the match and replace functions with generic Matcher, Replacer, TryMatcher, and TryReplacer functions
    • Removed get_matches_acc, match_and_replace_acc, and try_match_and_replace_acc functions, and replaced them with Matcher implementations
    • Added convenience implementations of Matcher and Replacer for Strings
    • Added AccumulatorMatcher and ChainMatcher convenience Matchers, TryMatcher variants of those, and the TryMatcherWrapper TryMatcher
    • Modularized most of the code and moved some modules to feature flags (enabled by default)
  • 2.2.1
    • Fixed an error which would occur when traversing strings containing characters spanning more than one byte
  • 2.2.0
    • Added try_replace_matches, try_match_and_replace, and try_match_and_replace_acc functions
  • 2.1.1
    • Fixed a major bug in the replacer functions
  • 2.1.0
    • Added get_matches_acc and match_and_replace_acc functions
  • 2.0.0
    • Added match_end parameter to matching functions
  • 1.0.1
    • Implemented Matchable for String
  • 1.0.0
    • Initial release

Re-exports§

pub use convenience::*;
pub use result::*;

Modules§

convenience
Additional Matcher and Replacer implementations including Strings, accumulator functions, and chaining sequential Matchers.
result
Allow matching and replacing with functions that return Results.

Structs§

InProgressMatch
The values for a potential substring match as it’s being processed and created.
Match
A substring match.

Enums§

MatchResult
The result of a substring matching char-level comparison.

Traits§

Matchable
Types with this trait can be matched and replaced using custom functions.
Matcher
Types within this trait can be used to find Matches in text.
Replacer
Types within this trait can be used to replace Matches from a text.