-
-
Notifications
You must be signed in to change notification settings - Fork 8
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
Open
T0mstone
wants to merge
12
commits into
typst:main
Choose a base branch
from
T0mstone:resolve
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a3a16a7
Reduce code duplication
T0mstone 536dc49
Add new API
T0mstone 4895855
Address comments
T0mstone 1fdbb1e
Use a more idiomatic module structure
T0mstone a5cbb19
Use IntoIterator instead of inherent methods
T0mstone 34edb09
Remove the macro
T0mstone ff6b56a
Add as_str
T0mstone 039f401
Revert "Add as_str"
T0mstone b99817a
Derive Eq, Hash
T0mstone 17d2dff
Reapply "Add as_str"
T0mstone 65e5b01
Fix iterator for empty `ModifierSet`
T0mstone 7157b82
Add more tests and clarify `best_match_in` docs
T0mstone File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
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)] | ||
// note: the visibility needs to be `pub(crate)`, | ||
// since build.rs outputs `ModifierSet(...)` | ||
pub struct ModifierSet<S>(pub(crate) S); | ||
|
||
impl<S: Default> Default for ModifierSet<S> { | ||
/// Construct 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<S: Deref<Target = str>> ModifierSet<S> { | ||
/// Convert the underlying string to a slice. | ||
pub fn as_deref(&self) -> ModifierSet<&str> { | ||
ModifierSet(&self.0) | ||
} | ||
|
||
/// Get the string of modifiers separated by `.`. | ||
pub fn as_str(&self) -> &str { | ||
&self.0 | ||
} | ||
|
||
/// Construct 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 { | ||
MDLC01 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Self(s) | ||
} | ||
|
||
/// Whether `self` is empty. | ||
pub fn is_empty(&self) -> bool { | ||
self.0.is_empty() | ||
} | ||
|
||
/// 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) | ||
where | ||
S: for<'a> std::ops::AddAssign<&'a str>, | ||
{ | ||
if !self.0.is_empty() { | ||
self.0 += "."; | ||
} | ||
self.0 += m; | ||
} | ||
|
||
/// Iterate 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) | ||
} | ||
|
||
/// 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)) | ||
} | ||
|
||
/// Find 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 | ||
} | ||
} | ||
|
||
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) | ||
); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is outdated since it uses
new_unchecked
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the point where it gets output is
codex/build.rs
Line 217 in 34edb09
which uses the debug-formatting, which outputs
ModifierSet(<string>)
.Changing that would of course be possible, but it'd require constructing the slice manually instead of just using
{:?}
there.