-
-
Notifications
You must be signed in to change notification settings - Fork 11
Codify and Resolve modifiers #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
a3a16a7
536dc49
4895855
1fdbb1e
a5cbb19
34edb09
ff6b56a
039f401
b99817a
17d2dff
65e5b01
7157b82
120d424
d7d2c96
6ef866a
477a343
1e8e94b
02ea0fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
use std::ops::Deref; | ||
|
||
/// A set of modifiers. | ||
/// | ||
/// Beware: The [`Eq`] and [`Hash`] implementations are dependent on the | ||
/// ordering of the modifiers, in opposition to what a set would usually | ||
/// constitute. To test for set-wise equality, use [`iter`](Self::iter) and | ||
/// collect into a true set type like [`HashSet`](std::collections::HashSet). | ||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] | ||
pub struct ModifierSet<S>( | ||
// Note: the visibility needs to be `pub(crate)`, since build.rs outputs | ||
// `ModifierSet(...)`. | ||
pub(crate) S, | ||
); | ||
|
||
impl<S: Deref<Target = str>> ModifierSet<S> { | ||
/// Constructs a modifier set from a string, where modifiers are separated by | ||
/// the character `.`. | ||
/// | ||
/// It is not unsafe to use this function wrongly, but it can produce | ||
/// unexpected results down the line. Correct usage should ensure that `s` | ||
/// does not contain any empty modifiers (i.e. the sequence `..`) and that | ||
/// no modifier occurs twice. | ||
pub fn new_unchecked(s: S) -> Self { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a fan of the term "unchecked" as it invokes unsafety feelings (even if documented). Also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not necessarily, e.g. |
||
Self(s) | ||
} | ||
|
||
/// Whether `self` is empty. | ||
pub fn is_empty(&self) -> bool { | ||
self.0.is_empty() | ||
} | ||
|
||
/// Gets the string of modifiers separated by `.`. | ||
pub fn as_str(&self) -> &str { | ||
&self.0 | ||
} | ||
|
||
/// Converts the underlying string to a slice. | ||
pub fn as_deref(&self) -> ModifierSet<&str> { | ||
ModifierSet(&self.0) | ||
} | ||
|
||
/// Add a modifier to the set, without checking that it is a valid modifier. | ||
/// | ||
/// It is not unsafe to use this method wrongly, but that can produce | ||
/// unexpected results down the line. Correct usage should ensure that | ||
/// `modifier` is not empty and doesn't contain the character `.`. | ||
pub fn add_unchecked(&mut self, m: &str) | ||
T0mstone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
where | ||
S: for<'a> std::ops::AddAssign<&'a str>, | ||
{ | ||
if !self.0.is_empty() { | ||
self.0 += "."; | ||
} | ||
self.0 += m; | ||
} | ||
|
||
/// Iterates over the list of modifiers in an arbitrary order. | ||
pub fn iter(&self) -> impl Iterator<Item = &str> { | ||
self.into_iter() | ||
} | ||
|
||
/// Whether the set contains the modifier `m`. | ||
pub fn contains(&self, m: &str) -> bool { | ||
self.iter().any(|lhs| lhs == m) | ||
} | ||
|
||
/// Finds the best match from the list. | ||
/// | ||
/// To be considered a match, the modifier set must be a superset of | ||
/// (or equal to) `self`. Among different matches, the best one is selected | ||
/// by the following two criteria (in order): | ||
/// 1. Number of modifiers in common with `self` (more is better). | ||
/// 2. Total number of modifiers (fewer is better). | ||
/// | ||
/// If there are multiple best matches, the first of them is returned. | ||
pub fn best_match_in<'a, T>( | ||
&self, | ||
variants: impl Iterator<Item = (ModifierSet<&'a str>, T)>, | ||
) -> Option<T> { | ||
let mut best = None; | ||
let mut best_score = None; | ||
|
||
// Find the best table entry with this name. | ||
for candidate in variants.filter(|(set, _)| self.is_subset(*set)) { | ||
let mut matching = 0; | ||
let mut total = 0; | ||
for modifier in candidate.0.iter() { | ||
if self.contains(modifier) { | ||
matching += 1; | ||
} | ||
total += 1; | ||
} | ||
|
||
let score = (matching, std::cmp::Reverse(total)); | ||
if best_score.is_none_or(|b| score > b) { | ||
best = Some(candidate.1); | ||
best_score = Some(score); | ||
} | ||
} | ||
|
||
best | ||
} | ||
|
||
/// Whether all modifiers in `self` are also present in `other`. | ||
pub fn is_subset(&self, other: ModifierSet<&str>) -> bool { | ||
self.iter().all(|m| other.contains(m)) | ||
} | ||
} | ||
|
||
impl<S: Default> Default for ModifierSet<S> { | ||
/// Constructs the default modifier set. | ||
/// | ||
/// This is typically the empty set, though the remark from | ||
/// [`Self::new_unchecked`] applies since `S::default()` could technically | ||
/// be anything. | ||
fn default() -> Self { | ||
Self(S::default()) | ||
} | ||
} | ||
|
||
impl<'a, S: Deref<Target = str>> IntoIterator for &'a ModifierSet<S> { | ||
type Item = &'a str; | ||
type IntoIter = std::str::Split<'a, char>; | ||
|
||
/// Iterate over the list of modifiers in an arbitrary order. | ||
fn into_iter(self) -> Self::IntoIter { | ||
let mut iter = self.0.split('.'); | ||
if self.0.is_empty() { | ||
// empty the iterator | ||
let _ = iter.next(); | ||
} | ||
iter | ||
} | ||
} | ||
|
||
impl<'a> IntoIterator for ModifierSet<&'a str> { | ||
type Item = &'a str; | ||
type IntoIter = std::str::Split<'a, char>; | ||
|
||
/// Iterate over the list of modifiers in an arbitrary order. | ||
fn into_iter(self) -> Self::IntoIter { | ||
let mut iter = self.0.split('.'); | ||
if self.0.is_empty() { | ||
// empty the iterator | ||
let _ = iter.next(); | ||
} | ||
iter | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
type ModifierSet = super::ModifierSet<&'static str>; | ||
|
||
#[test] | ||
fn default_is_empty() { | ||
assert!(ModifierSet::default().is_empty()); | ||
} | ||
|
||
#[test] | ||
fn iter_count() { | ||
assert_eq!(ModifierSet::default().iter().count(), 0); | ||
assert_eq!(ModifierSet::new_unchecked("a").iter().count(), 1); | ||
assert_eq!(ModifierSet::new_unchecked("a.b").iter().count(), 2); | ||
assert_eq!(ModifierSet::new_unchecked("a.b.c").iter().count(), 3); | ||
} | ||
|
||
#[test] | ||
fn subset() { | ||
assert!( | ||
ModifierSet::new_unchecked("a").is_subset(ModifierSet::new_unchecked("a.b")) | ||
); | ||
assert!( | ||
ModifierSet::new_unchecked("a").is_subset(ModifierSet::new_unchecked("b.a")) | ||
); | ||
assert!(ModifierSet::new_unchecked("a.b") | ||
.is_subset(ModifierSet::new_unchecked("b.c.a"))); | ||
} | ||
|
||
#[test] | ||
fn best_match() { | ||
// 1. more modifiers in common with self | ||
assert_eq!( | ||
ModifierSet::new_unchecked("a.b").best_match_in( | ||
[ | ||
(ModifierSet::new_unchecked("a.c"), 1), | ||
(ModifierSet::new_unchecked("a.b"), 2), | ||
] | ||
.into_iter() | ||
), | ||
Some(2) | ||
); | ||
// 2. fewer modifiers in general | ||
assert_eq!( | ||
ModifierSet::new_unchecked("a").best_match_in( | ||
[ | ||
(ModifierSet::new_unchecked("a"), 1), | ||
(ModifierSet::new_unchecked("a.b"), 2), | ||
] | ||
.into_iter() | ||
), | ||
Some(1) | ||
); | ||
// the first rule takes priority over the second | ||
assert_eq!( | ||
ModifierSet::new_unchecked("a.b").best_match_in( | ||
[ | ||
(ModifierSet::new_unchecked("a"), 1), | ||
(ModifierSet::new_unchecked("a.b"), 2), | ||
] | ||
.into_iter() | ||
), | ||
Some(2) | ||
); | ||
// among multiple best matches, the first one is returned | ||
assert_eq!( | ||
ModifierSet::default().best_match_in( | ||
[ | ||
(ModifierSet::new_unchecked("a"), 1), | ||
(ModifierSet::new_unchecked("b"), 2) | ||
] | ||
.into_iter() | ||
), | ||
Some(1) | ||
); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.