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:
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -78,6 +78,22 @@ pub async fn run(state: &AppState, doc_id: &str) -> AppResult<Vec<Entered>> {
|
||||
let params = us.map(|u| compose::params_for(u, p.kind(), st.doc.vote_floor));
|
||||
let now = util::now();
|
||||
let mut tx = state.db.begin().await?;
|
||||
// The text that enters is the text that was counted: a revision that landed
|
||||
// since the state was read stops it (the next pass will judge it again).
|
||||
let unchanged = sqlx::query(
|
||||
"UPDATE proposals SET updated_at = updated_at
|
||||
WHERE id = ? AND revision = ? AND title = ? AND body = ? AND status = ?",
|
||||
)
|
||||
.bind(&p.id)
|
||||
.bind(p.revision)
|
||||
.bind(&p.title)
|
||||
.bind(&p.body)
|
||||
.bind(&p.status)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if unchanged.rows_affected() != 1 {
|
||||
continue;
|
||||
}
|
||||
let kind = match p.kind() {
|
||||
ProposalKind::Text | ProposalKind::Remove => {
|
||||
// The displaced version goes back among the proposals, with its votes.
|
||||
|
||||
Reference in New Issue
Block a user