v3 : cinquième relecture — rien de ce qui touche un chiffre n'est une coquille

- Signature : tout caractère qui n'est pas une lettre latine et touche un
  chiffre compte, espaces et ponctuation comprises (« 5, 0 », « 1:5 »,
  « 1…5 ») ; un tiret compte devant ou après un chiffre à travers les espaces
  (« - 5 ») ; seules quatre espaces sont libres (ordinaire, insécable, fine
  insécable, saut de ligne).
- Négations complétées (guère, point, nothing, nobody, nadie, jamás…).
- Adoption : le texte qui entre en vigueur est celui qui a été compté — une
  révision arrivée pendant le passage l'arrête.
- Déploiement : « previous » est gardé avant le témoin de déploiement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-21 06:23:15 +02:00
co-authored by Claude Opus 5.5
parent 1eac6f856a
commit 6c0754a5c3
6 changed files with 63 additions and 21 deletions
+34 -12
View File
@@ -29,9 +29,9 @@ pub enum EditKind {
const NEGATIONS: &[&str] = &[
// fr
"ne", "n", "pas", "jamais", "non", "sans", "aucun", "aucune", "ni", "plus", "rien", "nul", "nulle", "personne",
// en
"not", "no", "never", "without", "none", "nor", // es
"nunca", "sin", "ningun", "ninguna", "ninguno", "tampoco", "nada",
"guere", "point", // en
"not", "no", "never", "without", "none", "nor", "nothing", "nobody", "nowhere", "neither", // es
"nunca", "sin", "ningun", "ninguna", "ninguno", "tampoco", "nada", "nadie", "jamas", "ni",
];
/// Words that carry a quantity, a scope or a relation (folded); changing one is
@@ -90,9 +90,9 @@ fn is_latin_letter(c: char) -> bool {
/// Ordinary punctuation, free to change in a typo fix — except where noted below.
const PUNCTUATION: &[char] =
&['.', ',', ';', ':', '!', '?', '\'', '’', '‘', '"', '“', '”', '«', '»', '…', '‐', '-', '–', '—', '*', '_'];
/// Separators that become part of a number between two figures ("10 000", "10,000", "182.625").
const NUMBER_SEPARATORS: &[char] = &['.', ',', '\'', '’'];
/// Dashes that become a sign next to a figure ("-5", "5-10").
/// The only spaces free to change; any other (hair, thin, ogham…) is kept as it is.
const FREE_SPACES: &[char] = &[' ', '\u{a0}', '\u{202f}', '\n', '\r', '\t'];
/// Dashes become a sign when a figure follows or precedes, even across spaces ("- 5").
const DASHES: &[char] = &['‐', '-', '–', '—'];
/// Invisible format characters (zero-width, bidirectional overrides and
@@ -109,22 +109,31 @@ pub fn has_hidden_controls(s: &str) -> bool {
/// Latin letter, a space or ordinary punctuation — figures in any script, signs,
/// relations, brackets, look-alikes (U+201A for a comma, U+2044 for a slash,
/// full-width forms), stray combining marks (U+0336, U+0338), other alphabets.
/// Spaces and punctuation count too where they belong to a number.
/// Anything that touches a figure counts too, spaces and punctuation included:
/// "5,0", "5,\u{202f}0", "1:5", "1…5" never read like "50" or "15".
fn signature(s: &str) -> String {
let cs: Vec<char> = s.nfc().collect();
let is_figure = |i: Option<usize>| i.and_then(|i| cs.get(i)).is_some_and(|c| c.is_numeric());
// The next character that is not a free space, on either side.
let across = |i: usize, step: isize| {
let mut j = i as isize + step;
while j >= 0 && (j as usize) < cs.len() && FREE_SPACES.contains(&cs[j as usize]) {
j += step;
}
j >= 0 && is_figure(Some(j as usize))
};
cs.iter()
.enumerate()
.filter(|&(i, &c)| {
let (before, after) = (is_figure(i.checked_sub(1)), is_figure(Some(i + 1)));
let touches_figure = is_figure(i.checked_sub(1)) || is_figure(Some(i + 1));
if is_latin_letter(c) {
false
} else if c.is_whitespace() {
before && after
} else if NUMBER_SEPARATORS.contains(&c) && before && after {
} else if touches_figure {
true
} else if FREE_SPACES.contains(&c) {
false
} else if DASHES.contains(&c) {
before || after
across(i, -1) || across(i, 1)
} else {
!PUNCTUATION.contains(&c)
}
@@ -412,6 +421,19 @@ mod tests {
("je certifierai", "je certifierais"),
("será válido", "sería válido"),
("they must certify", "they mustn certify"),
// Fifth independent review: anything touching a figure, special spaces, signs across spaces.
("au moins 50 voix", "au moins 5,\u{202f}0 voix"),
("**12 mois**", "**1,\u{202f}2 mois**"),
("100 certifications", "10,\u{200a}0 certifications"),
("80%", "8,\u{200a}0%"),
("15 jours", "1:5 jours"),
("15 jours", "1…5 jours"),
("15 jours", "1*5 jours"),
("15 jours", "1;5 jours"),
("15 jours", "1‘5 jours"),
("un écart de 5", "un écart de -\u{200a}5"),
("un écart de 5", "un écart de - 5"),
("un écart de 5", "un écart de \u{1680}5"),
] {
assert_eq!(classify(old, new), EditKind::Substantive, "{old} → {new}");
}