Skip to content

Commit

Permalink
Generics and borrowing
Browse files Browse the repository at this point in the history
  • Loading branch information
David Purdum committed Oct 8, 2021
1 parent 6522b25 commit f74aed7
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 154 deletions.
20 changes: 20 additions & 0 deletions notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Case conversion in steps
1. determine word boundaries
2. construct words
3. mutate words (letter casing)
4. join words

ccase
add option to treat each "word" as a muliword token
i.e. instead of word-one word-two -t snake = word_one_word_two
it is word_one word_two

String
+ Boundaries
-> Words
+ Delim + Pattern
-> Multiword Identifier

Cases are just aliases for Delim+Patterns (most of the time)
if not some other transformation (alternating)
Cases can also be mapped to boundaries
20 changes: 15 additions & 5 deletions src/boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ pub enum Boundary {
UpperDigit,
DigitLower,
LowerDigit,
//TwoChar(Box<dyn Fn(char, char) -> bool>),

//UpperUpperLower, // Acronyms
Acronyms,
//ThreeChar(Box<dyn Fn(char, char, char) -> bool>), // more complex, should include index
}

impl Boundary {
pub fn defaults() -> Vec<Self> {
use Boundary::*;
vec![
Underscore, Hyphen, Space,
LowerUpper, UpperDigit, DigitUpper,
DigitLower, LowerDigit, Acronyms,
]
}

fn detect_one(&self, c: char) -> bool {
use Boundary::*;
match self {
Expand Down Expand Up @@ -50,8 +56,12 @@ impl Boundary {
}
}

// idea: make a bitset for each boundary. Its fixed size,
// and can be copied. Also no fear in adding duplicates

// gross
pub fn split(s: &str, boundaries: &Vec<Boundary>) -> Vec<String> {
pub fn split<'a, T>(s: &'a T, boundaries: &Vec<Boundary>) -> Vec<&'a str> where T: AsRef<str> {
let s = s.as_ref();

let single_splits = s.chars().enumerate()
.filter(|(_, c)| boundaries.iter().any(|b| b.detect_one(*c)))
Expand Down Expand Up @@ -84,7 +94,7 @@ pub fn split(s: &str, boundaries: &Vec<Boundary>) -> Vec<String> {
split_on_indicies(w, splits)
});

final_words.rev().map(ToString::to_string).filter(|s| !s.is_empty()).collect()
final_words.rev().filter(|s| !s.is_empty()).collect()
}

pub fn replace_at_indicies(s: &str, splits: Vec<usize>) -> Vec<&str> {
Expand Down
173 changes: 173 additions & 0 deletions src/converter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use crate::boundary;
use crate::Boundary;
use crate::Pattern;
use crate::Case;

/// Holds information about parsing before converting into a case.
///
/// This struct is used when invoking the `from_case` method on
/// `Casing`.
/// ```
/// use convert_case::{Case, Casing};
///
/// let title = "ninety-nine_problems".from_case(Case::Snake).to_case(Case::Title);
/// assert_eq!("Ninety-nine Problems", title);
/// ```
pub struct StateConverter<'a, T: AsRef<str>> {
s: &'a T,
boundaries: Vec<Boundary>,
pattern: Option<Pattern>,
delim: String,
// delete the 3 above

// conv: Converter
}

impl<'a, T: AsRef<str>> StateConverter<'a, T> {
pub(crate) fn new(s: &'a T) -> Self {
Self {
s,
boundaries: Boundary::defaults(),
delim: String::new(),
pattern: None,
}
}

pub(crate) fn new_from_case(s: &'a T, case: Case) -> Self {
Self {
s,
boundaries: case.boundaries(),
delim: String::new(),
pattern: None, // doesn't matter
}
}

pub fn convert(self) -> String {
let words = boundary::split(&self.s, &self.boundaries);
if let Some(p) = self.pattern {
p.mutate(&words).join(&self.delim)
} else {
words.join(&self.delim)
}
}

pub fn to_case(mut self, case: Case) -> String {
self.pattern = Some(case.pattern());
self.delim = case.delim().to_string();
self.convert()
}

pub fn from_case(&mut self, case: Case) {
self.boundaries = case.boundaries();
}
}


/// Do something like this
pub struct Converter {
boundaries: Vec<Boundary>,
pattern: Option<Pattern>,
delim: String,
}

impl Default for Converter {
fn default() -> Self {
Converter {
boundaries: Boundary::defaults(),
pattern: None,
delim: String::new(),
}
}
}

impl Converter {
pub fn new() -> Self {
Self::default()
}

pub fn convert<T>(&self, s: T) -> String where T: AsRef<str> {
let words = boundary::split(&s, &self.boundaries);
if let Some(p) = self.pattern {
p.mutate(&words).join(&self.delim)
} else {
words.join(&self.delim)
}
}

pub fn to_case(mut self, case: Case) -> Self {
self.pattern = Some(case.pattern());
self.delim = case.delim().to_string();
self
}

pub fn from_case(mut self, case: Case) -> Self {
self.boundaries = case.boundaries();
self
}

pub fn add_boundary(mut self, b: &Boundary) -> Self {
self.boundaries.push(*b);
self
}

pub fn add_boundaries(mut self, bs: &Vec<Boundary>) -> Self {
self.boundaries.extend(bs);
self
}

pub fn set_boundaries(mut self, bs: &Vec<Boundary>) -> Self {
self.boundaries = bs.clone();
self
}

pub fn set_delim<T>(mut self, d: T) -> Self where T: ToString {
self.delim = d.to_string();
self
}

pub fn remove_delim(mut self) -> Self {
self.delim = String::new();
self
}

pub fn set_pattern(mut self, p: Pattern) -> Self {
self.pattern = Some(p);
self
}

pub fn remove_pattern(mut self) -> Self {
self.pattern = None;
self
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::Casing;
use crate::Pattern;

#[test]
fn snake_converter_from_case() {
let conv = Converter::new().to_case(Case::Snake);
let s = String::from("my var name");
assert_eq!(s.to_case(Case::Snake), conv.convert(s));
}

#[test]
fn snake_converter_from_scratch() {
let conv = Converter::new()
.set_delim("_")
.set_pattern(Pattern::Lowercase);
let s = String::from("my var name");
assert_eq!(s.to_case(Case::Snake), conv.convert(s));
}

#[test]
fn custom_pattern() {
let conv = Converter::new()
.to_case(Case::Snake)
.set_pattern(Pattern::Sentence);
assert_eq!("Bjarne_case", conv.convert("bjarne case"));
}
}
Loading

0 comments on commit f74aed7

Please sign in to comment.