feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
use alloc::string::String;
use core::cmp::Ordering;
use core::fmt;
use core::hash::{Hash, Hasher};
use core::ops::{Deref, DerefMut};
use core::str::FromStr;
use super::{Ascii, Encoding, UniCase};
impl<S> Ascii<S> {
/// Construct a new `Ascii`.
///
/// For Rust versions >= 1.31, this is a `const fn`.
#[inline]
pub const fn new(s: S) -> Ascii<S> {
Ascii(s)
}
/// Convert this into a [`UniCase`].
pub const fn into_unicase(self) -> UniCase<S> {
UniCase(Encoding::Ascii(self))
}
/// Consume this `Ascii` and get the inner value.
#[inline]
pub fn into_inner(self) -> S {
self.0
}
}
impl<S> Deref for Ascii<S> {
type Target = S;
#[inline]
fn deref<'a>(&'a self) -> &'a S {
&self.0
}
}
impl<S> DerefMut for Ascii<S> {
#[inline]
fn deref_mut<'a>(&'a mut self) -> &'a mut S {
&mut self.0
}
}
impl<T: AsRef<str>> PartialOrd for Ascii<T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<T: AsRef<str>> Ord for Ascii<T> {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
let self_chars = self.as_ref().chars().map(|c| c.to_ascii_lowercase());
let other_chars = other.as_ref().chars().map(|c| c.to_ascii_lowercase());
self_chars.cmp(other_chars)
}
}
impl<S: AsRef<str>> AsRef<str> for Ascii<S> {
#[inline]
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl<S: fmt::Display> fmt::Display for Ascii<S> {
#[inline]
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, fmt)
}
}
impl<S1: AsRef<str>> PartialEq<Ascii<S1>> for String {
#[inline]
fn eq(&self, other: &Ascii<S1>) -> bool {
other == self
}
}
impl<'a, S1: AsRef<str>> PartialEq<Ascii<S1>> for &'a str {
#[inline]
fn eq(&self, other: &Ascii<S1>) -> bool {
other == self
}
}
impl<S1: AsRef<str>, S2: AsRef<str>> PartialEq<S2> for Ascii<S1> {
#[inline]
fn eq(&self, other: &S2) -> bool {
self.as_ref().eq_ignore_ascii_case(other.as_ref())
}
}
impl<S: AsRef<str>> Eq for Ascii<S> {}
impl<S: FromStr> FromStr for Ascii<S> {
type Err = <S as FromStr>::Err;
fn from_str(s: &str) -> Result<Ascii<S>, <S as FromStr>::Err> {
s.parse().map(Ascii)
}
}
impl<S: AsRef<str>> Hash for Ascii<S> {
#[inline]
fn hash<H: Hasher>(&self, hasher: &mut H) {
for byte in self.as_ref().bytes().map(|b| b.to_ascii_lowercase()) {
hasher.write_u8(byte);
}
// prefix-freedom
hasher.write_u8(0xFF);
}
}
#[cfg(test)]
mod tests {
use super::Ascii;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::string::String;
fn hash<T: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
t.hash(&mut s);
s.finish()
}
#[test]
fn test_case_insensitive() {
let a = Ascii("foobar");
let b = Ascii("FOOBAR");
assert_eq!(a, b);
assert_eq!(hash(&a), hash(&b));
assert_eq!(a, "fooBar");
assert_eq!("fooBar", a);
assert_eq!(String::from("fooBar"), a);
assert_eq!(a, String::from("fooBar"));
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_ascii_eq(b: &mut ::test::Bencher) {
b.bytes = b"foobar".len() as u64;
b.iter(|| assert_eq!(Ascii("foobar"), Ascii("FOOBAR")));
}
#[test]
fn test_case_cmp() {
assert!(Ascii("foobar") == Ascii("FOOBAR"));
assert!(Ascii("a") < Ascii("B"));
assert!(Ascii("A") < Ascii("b"));
assert!(Ascii("aa") > Ascii("a"));
assert!(Ascii("a") < Ascii("aa"));
assert!(Ascii("a") < Ascii("AA"));
}
#[test]
fn test_ascii_new_const() {
const _ASCII: Ascii<&'static str> = Ascii::new("");
}
}
+469
View File
@@ -0,0 +1,469 @@
#![cfg_attr(test, deny(missing_docs))]
#![cfg_attr(test, deny(warnings))]
#![cfg_attr(feature = "nightly", feature(test))]
#![no_std]
//! # UniCase
//!
//! UniCase provides a way of specifying strings that are case-insensitive.
//!
//! UniCase supports full [Unicode case
//! folding](https://www.w3.org/International/wiki/Case_folding). It can also
//! utilize faster ASCII case comparisons, if both strings are ASCII.
//!
//! Using the `UniCase::new()` constructor will check the string to see if it
//! is all ASCII. When a `UniCase` is compared against another, if both are
//! ASCII, it will use the faster comparison.
//!
//! There also exists the `Ascii` type in this crate, which will always assume
//! to use the ASCII case comparisons, if the encoding is already known.
//!
//! ## Example
//!
//! ```rust
//! use unicase::UniCase;
//!
//! let a = UniCase::new("Maße");
//! let b = UniCase::new("MASSE");
//! let c = UniCase::new("mase");
//!
//! assert_eq!(a, b);
//! assert!(b != c);
//! ```
//!
//! ## Ascii
//!
//! ```rust
//! use unicase::Ascii;
//!
//! let a = Ascii::new("foobar");
//! let b = Ascii::new("FoObAr");
//!
//! assert_eq!(a, b);
//! ```
#[cfg(test)]
extern crate std;
#[cfg(feature = "nightly")]
extern crate test;
extern crate alloc;
use alloc::string::String;
use alloc::borrow::Cow;
use core::cmp::Ordering;
use core::fmt;
use core::hash::{Hash, Hasher};
use core::ops::{Deref, DerefMut};
use core::str::FromStr;
use self::unicode::Unicode;
mod ascii;
mod unicode;
/// Case Insensitive wrapper of strings.
#[derive(Clone, Copy)]
pub struct UniCase<S>(Encoding<S>);
/// Case Insensitive wrapper of Ascii strings.
#[derive(Clone, Copy, Debug, Default)]
pub struct Ascii<S>(S);
/// Compare two string-like types for case-less equality, using unicode folding.
///
/// Equivalent to `UniCase::new(left) == UniCase::new(right)`.
///
/// Note: This will perform a scan for ASCII characters before doing the
/// the comparison. See `UniCase` for more information.
#[inline]
pub fn eq<S: AsRef<str> + ?Sized>(left: &S, right: &S) -> bool {
UniCase::new(left) == UniCase::new(right)
}
/// Compare two string-like types for case-less equality, ignoring ASCII case.
///
/// Equivalent to `Ascii::new(left) == Ascii::new(right)`.
#[inline]
pub fn eq_ascii<S: AsRef<str> + ?Sized>(left: &S, right: &S) -> bool {
Ascii(left) == Ascii(right)
}
#[derive(Clone, Copy, Debug)]
enum Encoding<S> {
Ascii(Ascii<S>),
Unicode(Unicode<S>),
}
macro_rules! inner {
(mut $e:expr) => {{
match &mut $e {
&mut Encoding::Ascii(ref mut s) => &mut s.0,
&mut Encoding::Unicode(ref mut s) => &mut s.0,
}
}};
($e:expr) => {{
match &$e {
&Encoding::Ascii(ref s) => &s.0,
&Encoding::Unicode(ref s) => &s.0,
}
}};
}
impl<S: AsRef<str> + Default> Default for UniCase<S> {
fn default() -> Self {
Self::new(Default::default())
}
}
impl<S: AsRef<str>> UniCase<S> {
/// Creates a new `UniCase`.
///
/// Note: This scans the text to determine if it is all ASCII or not.
pub fn new(s: S) -> UniCase<S> {
if s.as_ref().is_ascii() {
UniCase(Encoding::Ascii(Ascii(s)))
} else {
UniCase(Encoding::Unicode(Unicode(s)))
}
}
/// Returns a copy of this string where each character is mapped to its
/// Unicode CaseFolding equivalent.
///
/// # Note
///
/// Unicode Case Folding is meant for string storage and matching, not for
/// display.
pub fn to_folded_case(&self) -> String {
match self.0 {
Encoding::Ascii(ref s) => s.0.as_ref().to_ascii_lowercase(),
Encoding::Unicode(ref s) => s.to_folded_case(),
}
}
}
impl<S> UniCase<S> {
/// Creates a new `UniCase`, skipping the ASCII check.
pub const fn unicode(s: S) -> UniCase<S> {
UniCase(Encoding::Unicode(Unicode(s)))
}
/// Creates a new `UniCase` which performs only ASCII case folding.
pub const fn ascii(s: S) -> UniCase<S> {
UniCase(Encoding::Ascii(Ascii(s)))
}
/// Return `true` if this instance will only perform ASCII case folding.
pub fn is_ascii(&self) -> bool {
match self.0 {
Encoding::Ascii(_) => true,
Encoding::Unicode(_) => false,
}
}
/// Unwraps the inner value held by this `UniCase`.
#[inline]
pub fn into_inner(self) -> S {
match self.0 {
Encoding::Ascii(s) => s.0,
Encoding::Unicode(s) => s.0,
}
}
}
impl<S> Deref for UniCase<S> {
type Target = S;
#[inline]
fn deref<'a>(&'a self) -> &'a S {
inner!(self.0)
}
}
impl<S> DerefMut for UniCase<S> {
#[inline]
fn deref_mut<'a>(&'a mut self) -> &'a mut S {
inner!(mut self.0)
}
}
impl<S: AsRef<str>> AsRef<str> for UniCase<S> {
#[inline]
fn as_ref(&self) -> &str {
inner!(self.0).as_ref()
}
}
impl<S: fmt::Debug> fmt::Debug for UniCase<S> {
#[inline]
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(inner!(self.0), fmt)
}
}
impl<S: fmt::Display> fmt::Display for UniCase<S> {
#[inline]
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(inner!(self.0), fmt)
}
}
impl<S1: AsRef<str>, S2: AsRef<str>> PartialEq<UniCase<S2>> for UniCase<S1> {
#[inline]
fn eq(&self, other: &UniCase<S2>) -> bool {
match (&self.0, &other.0) {
(&Encoding::Ascii(ref x), &Encoding::Ascii(ref y)) => x == y,
(&Encoding::Unicode(ref x), &Encoding::Unicode(ref y)) => x == y,
(&Encoding::Ascii(ref x), &Encoding::Unicode(ref y)) => &Unicode(x.as_ref()) == y,
(&Encoding::Unicode(ref x), &Encoding::Ascii(ref y)) => x == &Unicode(y.as_ref()),
}
}
}
impl<S: AsRef<str>> Eq for UniCase<S> {}
impl<S: AsRef<str>> Hash for UniCase<S> {
#[inline]
fn hash<H: Hasher>(&self, hasher: &mut H) {
match self.0 {
Encoding::Ascii(ref s) => s.hash(hasher),
Encoding::Unicode(ref s) => s.hash(hasher),
}
}
}
impl<S> From<Ascii<S>> for UniCase<S> {
fn from(ascii: Ascii<S>) -> Self {
UniCase(Encoding::Ascii(ascii))
}
}
macro_rules! from_impl {
($from:ty => $to:ty; $by:ident) => (
impl<'a> From<$from> for UniCase<$to> {
fn from(s: $from) -> Self {
UniCase::unicode(s.$by())
}
}
);
($from:ty => $to:ty) => ( from_impl!($from => $to; into); )
}
macro_rules! into_impl {
($to:ty) => {
impl<'a> Into<$to> for UniCase<$to> {
fn into(self) -> $to {
self.into_inner()
}
}
};
}
impl<S: AsRef<str>> From<S> for UniCase<S> {
fn from(s: S) -> Self {
UniCase::new(s)
}
}
from_impl!(&'a str => Cow<'a, str>);
from_impl!(String => Cow<'a, str>);
from_impl!(&'a str => String);
from_impl!(Cow<'a, str> => String; into_owned);
from_impl!(&'a String => &'a str; as_ref);
into_impl!(&'a str);
into_impl!(String);
into_impl!(Cow<'a, str>);
impl<T: AsRef<str>> PartialOrd for UniCase<T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<T: AsRef<str>> Ord for UniCase<T> {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
match (&self.0, &other.0) {
(&Encoding::Ascii(ref x), &Encoding::Ascii(ref y)) => x.cmp(y),
(&Encoding::Unicode(ref x), &Encoding::Unicode(ref y)) => x.cmp(y),
(&Encoding::Ascii(ref x), &Encoding::Unicode(ref y)) => {
Unicode(x.as_ref()).cmp(&Unicode(y.0.as_ref()))
}
(&Encoding::Unicode(ref x), &Encoding::Ascii(ref y)) => {
Unicode(x.0.as_ref()).cmp(&Unicode(y.as_ref()))
}
}
}
}
impl<S: FromStr + AsRef<str>> FromStr for UniCase<S> {
type Err = <S as FromStr>::Err;
fn from_str(s: &str) -> Result<UniCase<S>, Self::Err> {
s.parse().map(UniCase::new)
}
}
#[cfg(test)]
mod tests {
use super::UniCase;
use alloc::borrow::Cow;
use std::borrow::ToOwned;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::string::String;
fn hash<T: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
t.hash(&mut s);
s.finish()
}
#[test]
fn test_copy_for_refs() {
fn foo<T>(_: UniCase<T>) {}
let a = UniCase::new("foobar");
foo(a);
foo(a);
}
#[test]
fn test_eq_ascii() {
let a = UniCase::new("foobar");
let b = UniCase::new("FOOBAR");
let c = UniCase::ascii("FoObAr");
let d = UniCase::<&str>::from("foobar");
assert_eq!(a, b);
assert_eq!(a, c);
assert_eq!(a, d);
assert_eq!(b, a);
assert_eq!(b, c);
assert_eq!(b, d);
assert_eq!(c, a);
assert_eq!(c, b);
assert_eq!(c, d);
assert_eq!(d, a);
assert_eq!(d, b);
assert_eq!(d, c);
assert_eq!(hash(&a), hash(&b));
assert_eq!(hash(&a), hash(&c));
assert_eq!(hash(&a), hash(&d));
assert!(a.is_ascii());
assert!(b.is_ascii());
assert!(c.is_ascii());
assert!(d.is_ascii());
}
#[test]
fn test_str_ascii() {
// https://github.com/seanmonstar/unicase/issues/76
let a = UniCase::new("foobar");
let b = UniCase::<&str>::from("foobar");
let c = UniCase::<String>::from(String::from("foobar"));
let d = UniCase::<Cow<str>>::from(Cow::from("foobar"));
assert!(a.is_ascii());
assert!(b.is_ascii());
assert!(c.is_ascii());
assert!(d.is_ascii());
}
#[test]
fn test_eq_unicode() {
let a = UniCase::new("στιγμας");
let b = UniCase::new("στιγμασ");
assert_eq!(a, b);
assert_eq!(b, a);
assert_eq!(hash(&a), hash(&b));
}
#[test]
fn test_eq_unicode_left_is_substring() {
// https://github.com/seanmonstar/unicase/issues/38
let a = UniCase::unicode("foo");
let b = UniCase::unicode("foobar");
assert!(a != b);
assert!(b != a);
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_unicase_ascii(b: &mut ::test::Bencher) {
b.bytes = b"foobar".len() as u64;
let x = UniCase::new("foobar");
let y = UniCase::new("FOOBAR");
b.iter(|| assert_eq!(x, y));
}
#[cfg(feature = "nightly")]
static SUBJECT: &'static [u8] = b"ffoo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz foo bar baz oo bar baz quux herp derp";
#[cfg(feature = "nightly")]
#[inline(never)]
fn is_ascii(bytes: &[u8]) -> bool {
#[allow(unused, deprecated)]
use std::ascii::AsciiExt;
bytes.is_ascii()
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_is_ascii(b: &mut ::test::Bencher) {
b.iter(|| assert!(is_ascii(SUBJECT)));
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_is_utf8(b: &mut ::test::Bencher) {
b.iter(|| assert!(::std::str::from_utf8(SUBJECT).is_ok()));
}
#[test]
fn test_case_cmp() {
assert!(UniCase::new("a") < UniCase::new("B"));
assert!(UniCase::new("A") < UniCase::new("b"));
assert!(UniCase::new("aa") > UniCase::new("a"));
assert!(UniCase::new("a") < UniCase::new("aa"));
assert!(UniCase::new("a") < UniCase::new("AA"));
}
#[test]
fn test_from_impls() {
let view: &'static str = "foobar";
let _: UniCase<&'static str> = view.into();
let _: UniCase<&str> = view.into();
let _: UniCase<String> = view.into();
let owned: String = view.to_owned();
let _: UniCase<&str> = (&owned).into();
let _: UniCase<String> = owned.into();
}
#[test]
fn test_into_impls() {
let view: UniCase<&'static str> = UniCase::new("foobar");
let _: &'static str = view.into();
let _: &str = view.into();
let owned: UniCase<String> = "foobar".into();
let _: String = owned.clone().into();
let _: &str = owned.as_ref();
}
#[test]
fn test_unicase_unicode_const() {
const _UNICASE: UniCase<&'static str> = UniCase::unicode("");
}
}
+2025
View File
File diff suppressed because it is too large. Load diff
+216
View File
@@ -0,0 +1,216 @@
use alloc::string::String;
use core::cmp::Ordering;
use core::hash::{Hash, Hasher};
use self::map::lookup;
mod map;
#[derive(Clone, Copy, Debug, Default)]
pub struct Unicode<S>(pub S);
impl<S: AsRef<str>> Unicode<S> {
pub fn to_folded_case(&self) -> String {
self.0.as_ref().chars().flat_map(lookup).collect()
}
}
impl<S1: AsRef<str>, S2: AsRef<str>> PartialEq<Unicode<S2>> for Unicode<S1> {
#[inline]
fn eq(&self, other: &Unicode<S2>) -> bool {
let mut left = self.0.as_ref().chars().flat_map(lookup);
let mut right = other.0.as_ref().chars().flat_map(lookup);
// inline Iterator::eq since not added until Rust 1.5
loop {
let x = match left.next() {
None => return right.next().is_none(),
Some(val) => val,
};
let y = match right.next() {
None => return false,
Some(val) => val,
};
if x != y {
return false;
}
}
}
}
impl<S: AsRef<str>> Eq for Unicode<S> {}
impl<T: AsRef<str>> PartialOrd for Unicode<T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<T: AsRef<str>> Ord for Unicode<T> {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
let self_chars = self.0.as_ref().chars().flat_map(lookup);
let other_chars = other.0.as_ref().chars().flat_map(lookup);
self_chars.cmp(other_chars)
}
}
impl<S: AsRef<str>> Hash for Unicode<S> {
#[inline]
fn hash<H: Hasher>(&self, hasher: &mut H) {
let mut buf = [0; 4];
for c in self.0.as_ref().chars().flat_map(|c| lookup(c)) {
let len = char_to_utf8(c, &mut buf);
// we can't use `write(buf)` because the ASCII variant uses
// `write_u8`. The docs for Hash say that's technically different.
// ¯\_(ツ)_/¯
for &b in &buf[..len] {
hasher.write_u8(b);
}
}
// prefix-freedom
hasher.write_u8(0xFF);
}
}
#[inline]
fn char_to_utf8(c: char, dst: &mut [u8; 4]) -> usize {
const TAG_CONT: u8 = 0b1000_0000;
const TAG_TWO_B: u8 = 0b1100_0000;
const TAG_THREE_B: u8 = 0b1110_0000;
const TAG_FOUR_B: u8 = 0b1111_0000;
let code = c as u32;
if code <= 0x7F {
dst[0] = code as u8;
1
} else if code <= 0x7FF {
dst[0] = (code >> 6 & 0x1F) as u8 | TAG_TWO_B;
dst[1] = (code & 0x3F) as u8 | TAG_CONT;
2
} else if code <= 0xFFFF {
dst[0] = (code >> 12 & 0x0F) as u8 | TAG_THREE_B;
dst[1] = (code >> 6 & 0x3F) as u8 | TAG_CONT;
dst[2] = (code & 0x3F) as u8 | TAG_CONT;
3
} else {
dst[0] = (code >> 18 & 0x07) as u8 | TAG_FOUR_B;
dst[1] = (code >> 12 & 0x3F) as u8 | TAG_CONT;
dst[2] = (code >> 6 & 0x3F) as u8 | TAG_CONT;
dst[3] = (code & 0x3F) as u8 | TAG_CONT;
4
}
}
// internal mod so that the enum can be 'pub'
// thanks privacy-checker :___(
mod fold {
#[derive(Clone, Copy)]
pub enum Fold {
Zero,
One(char),
Two(char, char),
Three(char, char, char),
}
impl Iterator for Fold {
type Item = char;
#[inline]
fn next(&mut self) -> Option<char> {
match *self {
Fold::Zero => None,
Fold::One(one) => {
*self = Fold::Zero;
Some(one)
}
Fold::Two(one, two) => {
*self = Fold::One(two);
Some(one)
}
Fold::Three(one, two, three) => {
*self = Fold::Two(one, two);
Some(three)
}
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
match *self {
Fold::Zero => (0, Some(0)),
Fold::One(..) => (1, Some(1)),
Fold::Two(..) => (2, Some(2)),
Fold::Three(..) => (3, Some(3)),
}
}
}
impl From<(char,)> for Fold {
#[inline]
fn from((one,): (char,)) -> Fold {
Fold::One(one)
}
}
impl From<(char, char)> for Fold {
#[inline]
fn from((one, two): (char, char)) -> Fold {
Fold::Two(one, two)
}
}
impl From<(char, char, char)> for Fold {
#[inline]
fn from((one, two, three): (char, char, char)) -> Fold {
Fold::Three(one, two, three)
}
}
}
#[cfg(test)]
mod tests {
use super::Unicode;
macro_rules! eq {
($left:expr, $right:expr) => {{
assert_eq!(Unicode($left), Unicode($right));
}};
}
#[test]
fn test_ascii_folding() {
eq!("foo bar", "FoO BAR");
}
#[test]
fn test_simple_case_folding() {
eq!("στιγμας", "στιγμασ");
}
#[test]
fn test_full_case_folding() {
eq!("flour", "flour");
eq!("Maße", "MASSE");
eq!("ᾲ στο διάολο", "ὰι στο διάολο");
}
#[test]
fn test_to_folded_case() {
assert_eq!(Unicode("Maße").to_folded_case(), "masse");
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_ascii_folding(b: &mut ::test::Bencher) {
b.bytes = b"foo bar".len() as u64;
b.iter(|| eq!("foo bar", "FoO BAR"));
}
#[cfg(feature = "nightly")]
#[bench]
fn bench_simple_case_folding(b: &mut ::test::Bencher) {
b.bytes = "στιγμας".len() as u64;
b.iter(|| eq!("στιγμας", "στιγμασ"));
}
}