#![doc = include_str!("../README.md")]
#![no_std]
#![deny(missing_docs)]
#![deny(unsafe_code)]
#![warn(clippy::pedantic)]
extern crate alloc;
mod casing;
use alloc::string::String;
use casing::TITLECASE_TABLE;
use core::fmt::{Debug, Display, Formatter, Result, Write};
use core::iter::FusedIterator;
pub use tr_az::to_titlecase_tr_or_az;
use crate::tr_az::to_lowercase_tr_or_az;
#[allow(clippy::doc_link_with_quotes)]
#[must_use]
pub fn to_titlecase(c: char) -> [char; 3] {
if let Ok(index) = TITLECASE_TABLE.binary_search_by(|&(key, _)| key.cmp(&c)) {
TITLECASE_TABLE[index].1
} else {
[c, '\0', '\0']
}
}
pub trait TitleCase {
fn to_titlecase(self) -> ToTitleCase;
fn to_titlecase_tr_or_az(self) -> ToTitleCase;
fn is_titlecase(&self) -> bool;
}
impl TitleCase for char {
fn to_titlecase(self) -> ToTitleCase {
ToTitleCase(CaseMappingIter::new(to_titlecase(self)))
}
fn to_titlecase_tr_or_az(self) -> ToTitleCase {
ToTitleCase(CaseMappingIter::new(to_titlecase_tr_or_az(self)))
}
fn is_titlecase(&self) -> bool {
TITLECASE_TABLE
.binary_search_by(|&(key, _)| key.cmp(self))
.is_err()
}
}
pub trait StrTitleCase {
fn to_titlecase(&self) -> String;
fn to_titlecase_lower_rest(&self) -> String;
fn to_titlecase_tr_or_az(&self) -> String;
fn to_titlecase_tr_or_az_lower_rest(&self) -> String;
fn starts_titlecase(&self) -> bool;
fn starts_titlecase_rest_lower(&self) -> bool;
}
impl StrTitleCase for str {
fn to_titlecase(&self) -> String {
let mut iter = self.chars();
iter.next()
.into_iter()
.flat_map(TitleCase::to_titlecase)
.chain(iter)
.collect()
}
fn to_titlecase_lower_rest(&self) -> String {
let mut iter = self.chars();
iter.next()
.into_iter()
.flat_map(TitleCase::to_titlecase)
.chain(iter.flat_map(char::to_lowercase))
.collect()
}
fn to_titlecase_tr_or_az(&self) -> String {
let mut iter = self.chars();
iter.next()
.into_iter()
.flat_map(TitleCase::to_titlecase_tr_or_az)
.chain(iter)
.collect()
}
fn to_titlecase_tr_or_az_lower_rest(&self) -> String {
let mut iter = self.chars();
iter.next()
.into_iter()
.flat_map(TitleCase::to_titlecase_tr_or_az)
.chain(iter.map(to_lowercase_tr_or_az))
.collect()
}
fn starts_titlecase(&self) -> bool {
self.chars()
.next()
.as_ref()
.is_some_and(TitleCase::is_titlecase)
}
fn starts_titlecase_rest_lower(&self) -> bool {
let mut iter = self.chars();
iter.next().as_ref().is_some_and(TitleCase::is_titlecase) && iter.all(char::is_lowercase)
}
}
pub mod tr_az {
use alloc::string::String;
use core::fmt::{Display, Formatter, Result};
use core::iter::{once, FusedIterator};
use crate::{to_titlecase, CaseMappingIter};
#[must_use]
pub fn to_uppercase_tr_or_az(c: char) -> TrAzCaseMapper {
TrAzCaseMapper::new(
once(c)
.map(|c| match c {
'\u{0069}' => '\u{0130}', _ => c,
})
.flat_map(char::to_uppercase),
)
}
#[must_use]
pub fn to_lowercase_tr_or_az(c: char) -> char {
match c {
'\u{0049}' => '\u{0131}', '\u{0130}' => '\u{0069}', _ => c.to_lowercase().next().unwrap(), }
}
#[allow(clippy::doc_link_with_quotes)]
#[must_use]
pub fn to_titlecase_tr_or_az(c: char) -> [char; 3] {
if c == '\u{0069}' {
['\u{0130}', '\0', '\0']
} else {
to_titlecase(c)
}
}
pub trait TrAzCasing {
fn to_lowercase_tr_az(self) -> char;
fn is_lowercase_tr_az(&self) -> bool;
fn to_uppercase_tr_az(self) -> TrAzCaseMapper;
fn is_uppercase_tr_az(&self) -> bool;
}
impl TrAzCasing for char {
fn to_lowercase_tr_az(self) -> char {
to_lowercase_tr_or_az(self)
}
fn is_lowercase_tr_az(&self) -> bool {
self.is_lowercase()
}
fn to_uppercase_tr_az(self) -> TrAzCaseMapper {
to_uppercase_tr_or_az(self)
}
fn is_uppercase_tr_az(&self) -> bool {
self.is_uppercase()
}
}
pub trait StrTrAzCasing {
fn to_lowercase_tr_az(&self) -> String;
fn is_lowercase_tr_az(&self) -> bool;
fn to_uppercase_tr_az(&self) -> String;
fn is_uppercase_tr_az(&self) -> bool;
}
impl StrTrAzCasing for str {
fn to_lowercase_tr_az(&self) -> String {
self.chars().map(to_lowercase_tr_or_az).collect()
}
fn is_lowercase_tr_az(&self) -> bool {
self.chars().all(|c| c.is_lowercase_tr_az())
}
fn to_uppercase_tr_az(&self) -> String {
self.chars().flat_map(to_uppercase_tr_or_az).collect()
}
fn is_uppercase_tr_az(&self) -> bool {
self.chars().all(|c| c.is_uppercase_tr_az())
}
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct TrAzCaseMapper(CaseMappingIter);
impl TrAzCaseMapper {
fn new(mut chars: impl Iterator<Item = char>) -> Self {
TrAzCaseMapper(CaseMappingIter::new([
chars.next().unwrap_or('\0'),
chars.next().unwrap_or('\0'),
chars.next().unwrap_or('\0'),
]))
}
}
impl Iterator for TrAzCaseMapper {
type Item = char;
fn next(&mut self) -> Option<char> {
self.0.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for TrAzCaseMapper {
fn next_back(&mut self) -> Option<char> {
self.0.next_back()
}
}
impl FusedIterator for TrAzCaseMapper {}
impl ExactSizeIterator for TrAzCaseMapper {}
impl Display for TrAzCaseMapper {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
core::fmt::Display::fmt(&self.0, f)
}
}
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct ToTitleCase(CaseMappingIter);
impl Iterator for ToTitleCase {
type Item = char;
fn next(&mut self) -> Option<char> {
self.0.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for ToTitleCase {
fn next_back(&mut self) -> Option<char> {
self.0.next_back()
}
}
impl FusedIterator for ToTitleCase {}
impl ExactSizeIterator for ToTitleCase {}
impl Display for ToTitleCase {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
core::fmt::Display::fmt(&self.0, f)
}
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
enum CaseMappingIter {
Three(char, char, char),
Two(char, char),
One(char),
Zero,
}
impl CaseMappingIter {
fn new(chars: [char; 3]) -> CaseMappingIter {
if chars[2] == '\0' {
if chars[1] == '\0' {
CaseMappingIter::One(chars[0]) } else {
CaseMappingIter::Two(chars[0], chars[1])
}
} else {
CaseMappingIter::Three(chars[0], chars[1], chars[2])
}
}
}
impl Iterator for CaseMappingIter {
type Item = char;
fn next(&mut self) -> Option<char> {
match *self {
CaseMappingIter::Three(a, b, c) => {
*self = CaseMappingIter::Two(b, c);
Some(a)
}
CaseMappingIter::Two(b, c) => {
*self = CaseMappingIter::One(c);
Some(b)
}
CaseMappingIter::One(c) => {
*self = CaseMappingIter::Zero;
Some(c)
}
CaseMappingIter::Zero => None,
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let size = match self {
CaseMappingIter::Three(..) => 3,
CaseMappingIter::Two(..) => 2,
CaseMappingIter::One(_) => 1,
CaseMappingIter::Zero => 0,
};
(size, Some(size))
}
}
impl DoubleEndedIterator for CaseMappingIter {
fn next_back(&mut self) -> Option<char> {
match *self {
CaseMappingIter::Three(a, b, c) => {
*self = CaseMappingIter::Two(a, b);
Some(c)
}
CaseMappingIter::Two(b, c) => {
*self = CaseMappingIter::One(b);
Some(c)
}
CaseMappingIter::One(c) => {
*self = CaseMappingIter::Zero;
Some(c)
}
CaseMappingIter::Zero => None,
}
}
}
impl Display for CaseMappingIter {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match *self {
CaseMappingIter::Three(a, b, c) => {
f.write_char(a)?;
f.write_char(b)?;
f.write_char(c)
}
CaseMappingIter::Two(b, c) => {
f.write_char(b)?;
f.write_char(c)
}
CaseMappingIter::One(c) => f.write_char(c),
CaseMappingIter::Zero => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use crate::casing::TITLECASE_TABLE;
#[test]
fn self_mapping() {
TITLECASE_TABLE.iter().for_each(|(cp, mapping)| {
assert_ne!(*cp, mapping[0]);
});
}
#[test]
fn is_sorted() {
let mut last = '\0';
TITLECASE_TABLE.iter().for_each(|(cp, _)| {
assert!(*cp > last, "cp: {cp}, last: {last}");
last = *cp;
});
}
}