forked from yvv/decision
Lot T3c : les tests d'abord, rouges sur le texte de e7ab82f (8 tests) — « comme d'habitude » contre le terme défini « participation habituelle » ; une grille commune (grid, 85 100 cas : W de 100 à 10 000 000, médianes sous 1 % et au-dessus de 50 % de W ramenées par Participation::of, chaque quorum d'auteur de 2 à W jusqu'à 1 000, les cinq crans) à la place d'as_usual_grid, qui sautait les médianes hors des bornes ; l'écart entre la valeur bornée et la médiane (W 7 224, médiane 40, cran 2 : 28 « pour » sur 40, pas 27) ; la part exigée qui baisse au-delà du quorum de l'article ; « une seule voix » sur la grille ; quorum.rs : la participation et G inchangés par le quorum d'un article, le cran 3 d'un nouvel article
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,11 @@ fn numbers(levels: &Value, key: &str) -> Vec<u64> {
|
||||
levels.as_array().expect("levels").iter().map(|l| l[key].as_u64().expect("number")).collect()
|
||||
}
|
||||
|
||||
/// G of each level, as served.
|
||||
fn gradients(doc: &Value) -> Vec<Value> {
|
||||
doc["levels"].as_array().expect("levels").iter().map(|l| l["gradient"].clone()).collect()
|
||||
}
|
||||
|
||||
/// (quorum, quorumDefault, canSetQuorum) of a unit.
|
||||
fn quorum_of(u: &Value) -> (Option<u64>, Option<u64>, Option<bool>) {
|
||||
(u["quorum"].as_u64(), u["quorumDefault"].as_u64(), u["canSetQuorum"].as_bool())
|
||||
@@ -174,6 +179,8 @@ async fn the_author_of_an_article_sets_its_quorum_before_the_first_vote() {
|
||||
.await;
|
||||
let (u, text) =
|
||||
(created["unitId"].as_str().expect("unit").to_owned(), created["id"].as_str().expect("id").to_owned());
|
||||
// « Tout nouvel article commence à ce cran » (F1.2, « Par défaut »): the default level, 3.
|
||||
assert_eq!(unit_by_id(&chiara.doc("essai").await, &u)["inertia"], 3);
|
||||
// Its author may, another member may not; the test group: its level's quorum, 1.
|
||||
assert_eq!(quorum_of(unit_by_id(&chiara.doc("essai").await, &u)), (None, Some(1), Some(true)));
|
||||
assert_eq!(quorum_of(unit_by_id(&luna.doc("essai").await, &u)), (None, Some(1), Some(false)));
|
||||
@@ -270,6 +277,7 @@ async fn in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pen
|
||||
chiara.login("Chiara", false).await;
|
||||
let doc = admin.doc("monnaie-libre").await;
|
||||
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
|
||||
let usual = (doc["doc"]["participation"].clone(), gradients(&doc));
|
||||
let i1 = unit(&doc, "I1");
|
||||
let (u, p) = (i1["id"].as_str().expect("u").to_owned(), i1["proposals"][0]["id"].as_str().expect("p").to_owned());
|
||||
// A text of the seed has no author: an administrator answers for it.
|
||||
@@ -287,6 +295,10 @@ async fn in_monnaie_libre_a_quorum_holds_two_voices_and_a_ballot_counts_even_pen
|
||||
}
|
||||
let doc = admin.doc("monnaie-libre").await;
|
||||
assert_eq!(unit(&doc, "I1")["proposals"][0]["evaluation"]["quorum"], 20);
|
||||
// « Elle ne dépend pas du quorum d'un article » (N1.1, lot T3c): the participation habituelle,
|
||||
// and G of each level, stay as they were (the reference rests on the document's, 2 × 50 / W).
|
||||
assert_eq!((doc["doc"]["participation"].clone(), gradients(&doc)), usual);
|
||||
assert!((doc["doc"]["participation"]["ratio"].as_f64().expect("p̃") - 100.0 / W as f64).abs() < 1e-12);
|
||||
// A refused ballot, an expired one, are no vote.
|
||||
ballot(&state, &doc_id, &p, "refused", 1).await;
|
||||
ballot(&state, &doc_id, &p, "expired", 2).await;
|
||||
|
||||
@@ -27,6 +27,18 @@
|
||||
//! group votes as usual — « à la participation habituelle » was false under the quorum; « autant de
|
||||
//! « pour » » in French; and « 100 votants » is checked as the parameter it is, not as a turnout
|
||||
//! that would hold on every W.
|
||||
//!
|
||||
//! After the review of e7ab82f (lot T3c, before it went online: the same amendments, their
|
||||
//! fingerprints `to` changed again): « comme d'habitude » reads first as the real median, and is
|
||||
//! false where the median leaves its bounds (W 7 224, a median of 40, level 2, quorum 12: 28 « pour »
|
||||
//! of 40, not 27). F1.2 and N1.1 write the defined term, « à la participation habituelle »: the
|
||||
//! median brought back within 1 % and 50 % of W, the one G is computed on. « Sous le quorum de
|
||||
//! l'article » where a bare « quorum » could be the level's; « la part exigée » where « la part »
|
||||
//! could be the rounded share; « Tant qu'elle repose sur moins de 5 propositions » (the year counts).
|
||||
//! Every sentence of F1.2 and N1.1 that says a number, a share or a direction is checked against the
|
||||
//! engine on one grid (`grid`): W from 100, medians under 1 % and above 50 % of W, every quorum an
|
||||
//! author may set, the five levels — the closed list given to the reviewer is
|
||||
//! `docs/chantiers/acte-certification/t3c-liste-fermee.md`.
|
||||
|
||||
use ld_engine::inertia::{InertiaLevel, Participation, Scale, gradient, quorum_bounds};
|
||||
use ld_engine::threshold::{FormulaParams, Tally, judged_voters, passes, required_ratio, wot_threshold, yes_needed};
|
||||
@@ -302,7 +314,7 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
|
||||
),
|
||||
format!("**B = {}**", dec(lang, f.base)),
|
||||
format!(
|
||||
"Le cran par défaut est le {} ({}) : M = {}, un quorum de {qd} dans Monnaie libre, et, au-delà du quorum, le groupe demande {} de « pour » quand il vote comme d'habitude",
|
||||
"Le cran par défaut est le {} ({}) : M = {}, un quorum de {qd} dans Monnaie libre, et, au-delà du quorum, le groupe demande {} de « pour » à sa participation habituelle",
|
||||
f.default_level,
|
||||
name(f.default_level),
|
||||
pct(lang, d.majority_pct()),
|
||||
@@ -330,7 +342,7 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
|
||||
format!("In Monnaie libre, never less than {}: a single vote never passes a text", f.lowest_quorum),
|
||||
format!("**B = {}**", dec(lang, f.base)),
|
||||
format!(
|
||||
"The default level is {} ({}): M = {}, a quorum of {qd} in Monnaie libre, and, beyond the quorum, the group asks for {} of votes in favour when it votes as usual",
|
||||
"The default level is {} ({}): M = {}, a quorum of {qd} in Monnaie libre, and, beyond the quorum, the group asks for {} of votes in favour at its usual turnout",
|
||||
f.default_level,
|
||||
name(f.default_level),
|
||||
pct(lang, d.majority_pct()),
|
||||
@@ -358,7 +370,7 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
|
||||
format!("En Monnaie libre, nunca menos de {}: un solo voto nunca hace pasar un texto", f.lowest_quorum),
|
||||
format!("**B = {}**", dec(lang, f.base)),
|
||||
format!(
|
||||
"El nivel por defecto es el {} ({}): M = {}, un quórum de {qd} en Monnaie libre, y, más allá del quórum, el grupo pide un {} de votos a favor cuando vota como de costumbre",
|
||||
"El nivel por defecto es el {} ({}): M = {}, un quórum de {qd} en Monnaie libre, y, más allá del quórum, el grupo pide un {} de votos a favor con su participación habitual",
|
||||
f.default_level,
|
||||
name(f.default_level),
|
||||
pct(lang, d.majority_pct()),
|
||||
@@ -390,17 +402,17 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
|
||||
);
|
||||
match lang {
|
||||
"fr" => format!(
|
||||
"- **Cran {} · {n}** : quorum {} ; {s} comme d'habitude, {mk} quand tout le monde vote. {what}",
|
||||
"- **Cran {} · {n}** : quorum {} ; {s} à la participation habituelle, {mk} quand tout le monde vote. {what}",
|
||||
k.get(),
|
||||
q[i]
|
||||
),
|
||||
"en" => format!(
|
||||
"- **Level {} · {n}**: quorum {}; {s} as usual, {mk} when everyone votes. {what}",
|
||||
"- **Level {} · {n}**: quorum {}; {s} at the usual turnout, {mk} when everyone votes. {what}",
|
||||
k.get(),
|
||||
q[i]
|
||||
),
|
||||
_ => format!(
|
||||
"- **Nivel {} · {n}**: quórum {}; {s} como de costumbre, {mk} cuando vota todo el mundo. {what}",
|
||||
"- **Nivel {} · {n}**: quórum {}; {s} con la participación habitual, {mk} cuando vota todo el mundo. {what}",
|
||||
k.get(),
|
||||
q[i]
|
||||
),
|
||||
@@ -416,20 +428,20 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
|
||||
out.extend(match lang {
|
||||
"fr" => [
|
||||
"propositions du groupe votées depuis un an".to_owned(),
|
||||
format!("Tant que le groupe n'a pas voté sur {ms} propositions, elle vaut deux fois le quorum de référence du document : {r} votants dans Monnaie libre"),
|
||||
format!("Elle reste entre {b0} et {b1} de l'électorat, et se mesure une fois par jour"),
|
||||
format!("Tant qu'elle repose sur moins de {ms} propositions, elle vaut deux fois le quorum de référence du document : {r} votants dans Monnaie libre"),
|
||||
format!("Elle est ramenée entre {b0} et {b1} de l'électorat, et se mesure une fois par jour"),
|
||||
format!("où s est cette part du cran ({head} ou {last})"),
|
||||
],
|
||||
"en" => [
|
||||
"the group's proposals voted on over the past year".to_owned(),
|
||||
format!("Until the group has voted on {ms} proposals, it is twice the document's reference quorum: {r} voters in Monnaie libre"),
|
||||
format!("It stays between {b0} and {b1} of the electorate, and is measured once a day"),
|
||||
format!("While it rests on fewer than {ms} proposals, it is twice the document's reference quorum: {r} voters in Monnaie libre"),
|
||||
format!("It is kept between {b0} and {b1} of the electorate, and is measured once a day"),
|
||||
format!("where s is that share of the level ({head} or {last})"),
|
||||
],
|
||||
_ => [
|
||||
"propuestas del grupo votadas en el último año".to_owned(),
|
||||
format!("Hasta que el grupo haya votado {ms} propuestas, vale dos veces el quórum de referencia del documento: {r} votantes en Monnaie libre"),
|
||||
format!("Se mantiene entre el {b0} y el {b1} del electorado, y se mide una vez al día"),
|
||||
format!("Mientras se base en menos de {ms} propuestas, vale dos veces el quórum de referencia del documento: {r} votantes en Monnaie libre"),
|
||||
format!("Se acota entre el {b0} y el {b1} del electorado, y se mide una vez al día"),
|
||||
format!("donde s es esa proporción del nivel ({head} o {last})"),
|
||||
],
|
||||
});
|
||||
@@ -619,63 +631,129 @@ fn the_three_phrases_of_the_review_of_m6b_are_written_true() {
|
||||
// Lot T3: no dated figure, every sentence true for every W and every quorum
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// (article, language, the words of T2, the words of today): the only change of lot T3, with its
|
||||
/// review (2ce2028: the sentence of G; « autant de « pour » » in French). F1.1 has none: it holds
|
||||
/// no dated figure and every sentence of it is true (`b_to_the_power_w_…`).
|
||||
const PHRASES_T3: [(&str, &str, &str, &str); 15] = [
|
||||
/// (article, language, the words of T2, the words of today): the only change of the amendments of
|
||||
/// lot T3, with its reviews — 2ce2028 (lot T3b: the sentence of G; « autant de « pour » » in French)
|
||||
/// and e7ab82f (lot T3c: the defined term « participation habituelle » where T3 wrote « comme
|
||||
/// d'habitude »; « le quorum de l'article » where a bare « quorum » could be the level's; « la part
|
||||
/// exigée »; « repose sur moins de 5 propositions »; « ramenée entre 1 % et 50 % »). The five lines
|
||||
/// of the levels change by `LEVEL_LINES_T3`. F1.1 has none: it holds no dated figure and every
|
||||
/// sentence of it is true (`b_to_the_power_w_…`).
|
||||
const PHRASES_T3: [(&str, &str, &str, &str); 27] = [
|
||||
(
|
||||
"F1.2",
|
||||
"fr",
|
||||
"un quorum de 50 dans Monnaie libre, et le groupe demande 75 % de « pour » quand il vote comme d'habitude.",
|
||||
"un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » quand il vote comme d'habitude.",
|
||||
"un quorum de 50 dans Monnaie libre, et, au-delà du quorum, le groupe demande 75 % de « pour » à sa participation habituelle.",
|
||||
),
|
||||
(
|
||||
"F1.2",
|
||||
"en",
|
||||
"a quorum of 50 in Monnaie libre, and the group asks for 75% of votes in favour when it votes as usual.",
|
||||
"a quorum of 50 in Monnaie libre, and, beyond the quorum, the group asks for 75% of votes in favour when it votes as usual.",
|
||||
"a quorum of 50 in Monnaie libre, and, beyond the quorum, the group asks for 75% of votes in favour at its usual turnout.",
|
||||
),
|
||||
(
|
||||
"F1.2",
|
||||
"es",
|
||||
"un quórum de 50 en Monnaie libre, y el grupo pide un 75 % de votos a favor cuando vota como de costumbre.",
|
||||
"un quórum de 50 en Monnaie libre, y, más allá del quórum, el grupo pide un 75 % de votos a favor cuando vota como de costumbre.",
|
||||
"un quórum de 50 en Monnaie libre, y, más allá del quórum, el grupo pide un 75 % de votos a favor con su participación habitual.",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"fr",
|
||||
"Chaque cran a aussi son quorum, qui n'est pas un nombre minimal de votants : sous le quorum, les votants qui manquent comptent comme « non ».",
|
||||
"Chaque cran a aussi son quorum : sous le quorum, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si le quorum avait voté.",
|
||||
"Chaque cran a aussi son quorum : sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté.",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"en",
|
||||
"Each level also has its quorum, which is not a minimum number of voters: below the quorum, the voters who are missing count as votes against.",
|
||||
"Each level also has its quorum: below the quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if the quorum had voted.",
|
||||
"Each level also has its quorum: below the article's quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if that quorum had voted.",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"es",
|
||||
"Cada nivel tiene además su quórum, que no es un número mínimo de votantes: por debajo del quórum, los votantes que faltan cuentan como «no».",
|
||||
"Cada nivel tiene además su quórum: por debajo del quórum, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado el quórum.",
|
||||
"Cada nivel tiene además su quórum: por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum.",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"fr",
|
||||
"La part dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà du quorum, plus il y a de votants,",
|
||||
"La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà de ce quorum, plus il y a de votants,",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"en",
|
||||
"The share depends on the level and on the number of voters, compared with the group's usual turnout: beyond the quorum, the more voters,",
|
||||
"The share asked depends on the level and on the number of voters, compared with the group's usual turnout: beyond that quorum, the more voters,",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"es",
|
||||
"La proporción depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá del quórum, cuantos más votantes,",
|
||||
"La proporción exigida depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá de ese quórum, cuantos más votantes,",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"fr",
|
||||
"où T' est le nombre de votants, ou le quorum s'il en manque.",
|
||||
"où T' est le nombre de votants, ou le quorum de l'article s'il en manque.",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"en",
|
||||
"where T' is the number of voters, or the quorum if some are missing.",
|
||||
"where T' is the number of voters, or the article's quorum if some are missing.",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"es",
|
||||
"donde T' es el número de votantes, o el quórum si faltan.",
|
||||
"donde T' es el número de votantes, o el quórum del artículo si faltan.",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"fr",
|
||||
"**Les cinq crans** : le quorum dans Monnaie libre, la part exigée quand le groupe vote comme d'habitude, puis quand tout le monde vote,",
|
||||
"**Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum, quand le groupe vote comme d'habitude, puis quand tout le monde vote,",
|
||||
"**Les cinq crans** : le quorum dans Monnaie libre, la part exigée au-delà du quorum de l'article, à la participation habituelle, puis quand tout le monde vote,",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"en",
|
||||
"**The five levels**: the quorum in Monnaie libre, the share asked when the group votes as usual, then when everyone votes,",
|
||||
"**The five levels**: the quorum in Monnaie libre, the share asked beyond the quorum, when the group votes as usual, then when everyone votes,",
|
||||
"**The five levels**: the quorum in Monnaie libre, the share asked beyond the article's quorum, at the usual turnout, then when everyone votes,",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"es",
|
||||
"**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida cuando el grupo vota como de costumbre, luego cuando vota todo el mundo,",
|
||||
"**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida más allá del quórum, cuando el grupo vota como de costumbre, luego cuando vota todo el mundo,",
|
||||
"**Los cinco niveles**: el quórum en Monnaie libre, la proporción exigida más allá del quórum del artículo, con la participación habitual, luego cuando vota todo el mundo,",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"fr",
|
||||
"Tant que le groupe n'a pas voté sur 5 propositions, elle vaut",
|
||||
"Tant qu'elle repose sur moins de 5 propositions, elle vaut",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"en",
|
||||
"Until the group has voted on 5 proposals, it is",
|
||||
"While it rests on fewer than 5 proposals, it is",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"es",
|
||||
"Hasta que el grupo haya votado 5 propuestas, vale",
|
||||
"Mientras se base en menos de 5 propuestas, vale",
|
||||
),
|
||||
("N1.1", "fr", "Elle reste entre 1 % et 50 % de l'électorat", "Elle est ramenée entre 1 % et 50 % de l'électorat"),
|
||||
("N1.1", "en", "It stays between 1% and 50% of the electorate", "It is kept between 1% and 50% of the electorate"),
|
||||
(
|
||||
"N1.1",
|
||||
"es",
|
||||
"Se mantiene entre el 1 % y el 50 % del electorado",
|
||||
"Se acota entre el 1 % y el 50 % del electorado",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
@@ -699,22 +777,31 @@ const PHRASES_T3: [(&str, &str, &str, &str); 15] = [
|
||||
"N1.1",
|
||||
"fr",
|
||||
"**G** se calcule pour que la part exigée, à la participation habituelle, soit exactement celle du cran : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est la part exigée comme d'habitude et p̃ la participation habituelle divisée par W.",
|
||||
"**G** se calcule pour que la part exigée, au-delà du quorum, soit exactement celle du cran quand le groupe vote comme d'habitude : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W.",
|
||||
"**G** se calcule pour que la part exigée, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle : `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, où s est cette part du cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W.",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"en",
|
||||
"**G** is computed so that the share asked, at the usual turnout, is exactly that of the level: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is the share asked as usual and p̃ the usual turnout divided by W.",
|
||||
"**G** is computed so that the share asked, beyond the quorum, is exactly that of the level when the group votes as usual: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is that share of the level (60, 67, 75, 83 or 90%) and p̃ the usual turnout divided by W.",
|
||||
"**G** is computed so that the share asked, beyond the article's quorum, is exactly that of the level at the usual turnout: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, where s is that share of the level (60, 67, 75, 83 or 90%) and p̃ the usual turnout divided by W.",
|
||||
),
|
||||
(
|
||||
"N1.1",
|
||||
"es",
|
||||
"**G** se calcula para que la proporción exigida, con la participación habitual, sea exactamente la del nivel: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es la proporción exigida como de costumbre y p̃ la participación habitual dividida por W.",
|
||||
"**G** se calcula para que la proporción exigida, más allá del quórum, sea exactamente la del nivel cuando el grupo vota como de costumbre: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es esa proporción del nivel (60, 67, 75, 83 o 90 %) y p̃ la participación habitual dividida por W.",
|
||||
"**G** se calcula para que la proporción exigida, más allá del quórum del artículo, sea exactamente la del nivel con la participación habitual: `G = ln(1 − (s − M)/(1 − M)) / ln(p̃)`, donde s es esa proporción del nivel (60, 67, 75, 83 o 90 %) y p̃ la participación habitual dividida por W.",
|
||||
),
|
||||
];
|
||||
|
||||
/// The five lines of the levels of N1.1 (lot T3c): « 60 % comme d'habitude » becomes « 60 % à la
|
||||
/// participation habituelle », and so on, in each line and nothing else (language, words of T2,
|
||||
/// words of today).
|
||||
const LEVEL_LINES_T3: [(&str, &str, &str); 3] = [
|
||||
("fr", " comme d'habitude, ", " à la participation habituelle, "),
|
||||
("en", " as usual, ", " at the usual turnout, "),
|
||||
("es", " como de costumbre, ", " con la participación habitual, "),
|
||||
];
|
||||
|
||||
/// The texts of T2 with the phrases of lot T3 written in: what the seed gives today.
|
||||
fn t3_from_t2(code: &str, lang: &str, t2: &str) -> String {
|
||||
let mut b = t2.to_owned();
|
||||
@@ -724,6 +811,11 @@ fn t3_from_t2(code: &str, lang: &str, t2: &str) -> String {
|
||||
b = b.replacen(old, new, 1);
|
||||
}
|
||||
}
|
||||
if code == "N1.1" {
|
||||
let (_, old, new) = LEVEL_LINES_T3.iter().find(|(l, ..)| *l == lang).expect("a language");
|
||||
assert_eq!(b.matches(old).count(), 5, "N1.1 {lang}: the five lines of the levels");
|
||||
b = b.replace(old, new);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
@@ -742,33 +834,192 @@ fn the_texts_of_lot_t3_change_these_phrases_only() {
|
||||
assert!(!body.contains(old), "{code} {lang}: still the words of T2\n{body}");
|
||||
assert!(body.contains(new), "{code} {lang}: missing « {new} »\n{body}");
|
||||
}
|
||||
for (lang, old, new) in LEVEL_LINES_T3 {
|
||||
let (_, body) = text(&today, "N1.1", lang);
|
||||
assert!(!body.contains(old), "N1.1 {lang}: « {old} » is left\n{body}");
|
||||
assert_eq!(body.matches(new).count(), 6, "N1.1 {lang}: the five lines, and the list's introduction\n{body}");
|
||||
}
|
||||
// The example of N1.1 is gone whole, and nothing takes its place.
|
||||
for (lang, word) in [("fr", "Exemple"), ("en", "Example"), ("es", "Ejemplo")] {
|
||||
assert!(!text(&today, "N1.1", lang).1.contains(word), "N1.1 {lang}: an example is left");
|
||||
}
|
||||
}
|
||||
|
||||
/// The participations of the grid, as shares of W: the bounds, the reference of the document
|
||||
/// (`2 × voteFloor / W`, brought back within the bounds) and one between.
|
||||
fn participations(w: u32, vote_floor: f64) -> [f64; 4] {
|
||||
[Participation::LOWEST, Participation::of(w, vote_floor, None, 0).ratio, 0.1, Participation::HIGHEST]
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lot T3c: one grid for every sentence that says a number, a share or a direction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The medians of the grid on a web of `w` members (lot T3c), as measured, before the bounds:
|
||||
/// under 1 % of W, at it, within, at 50 % and above. The participation habituelle is the median
|
||||
/// brought back within its bounds, so a median out of them is not it.
|
||||
fn medians(w: u32) -> Vec<u32> {
|
||||
let one = w / 100;
|
||||
let mut m: Vec<u32> = [1, 2, one.saturating_sub(1), one, one + 1, 12, 40, 50, 100, 120, 1000, w / 2, w / 2 + 1, w]
|
||||
.into_iter()
|
||||
.filter(|m| (1..=w).contains(m))
|
||||
.collect();
|
||||
m.sort_unstable();
|
||||
m.dedup();
|
||||
m
|
||||
}
|
||||
|
||||
/// N1.1, the phrase of lot T3: « sous le quorum, les votants qui manquent comptent comme « non »,
|
||||
/// et il faut autant de « pour » que si le quorum avait voté » (after the review of 2ce2028: « de
|
||||
/// voix pour que » read first as « pour que », a conjunction). True for every quorum an author
|
||||
/// may set (2 to W in Monnaie libre, 1 to W in the test group), every level, every W, every
|
||||
/// participation within the bounds: the « pour » that pass with nobody against, fewer voters
|
||||
/// than the quorum, are the threshold of the quorum; and under it, a tally passes exactly when
|
||||
/// its « pour » reach that threshold. The phrase of T2, « qui n'est pas un nombre minimal de
|
||||
/// votants », is false: at a quorum of 2, two « pour » are asked, at every level, on every W.
|
||||
/// A usual turnout of the grid: the median measured (`None`: fewer than 5 proposals, the reference
|
||||
/// of the document stands in) and the participation habituelle the engine takes from it
|
||||
/// (`Participation::of`: the median, or the reference, brought back within 1 % and 50 % of W).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Usual {
|
||||
median: Option<u32>,
|
||||
part: Participation,
|
||||
}
|
||||
|
||||
impl Usual {
|
||||
/// The participation habituelle, in voters: p̃ × W — not a whole number when it is a bound
|
||||
/// (1 % of 7 224 is 72,24 voters). To a millionth of a voter: the division by W and the product
|
||||
/// by W give 57,999 999 999 999 99 for a median of 58 on 5 707.
|
||||
fn voters(&self, w: u32) -> f64 {
|
||||
(self.part.voters(w) * 1e6).round() / 1e6
|
||||
}
|
||||
}
|
||||
|
||||
/// The usual turnouts of the grid on a web of `w`: the reference, then each median of `medians`.
|
||||
fn usuals(w: u32, vote_floor: f64) -> Vec<Usual> {
|
||||
std::iter::once(None)
|
||||
.chain(medians(w).into_iter().map(Some))
|
||||
.map(|median| {
|
||||
let sample = if median.is_some() { 9 } else { 0 };
|
||||
Usual { median, part: Participation::of(w, vote_floor, median.map(f64::from), sample) }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The participations of the grid, as shares of W, each once: those of `usuals`. Both bounds are
|
||||
/// among them on every W of Monnaie libre, where a median under 1 % or above 50 % of W is brought
|
||||
/// back to one.
|
||||
fn participations(w: u32, vote_floor: f64) -> Vec<f64> {
|
||||
let mut p: Vec<f64> = usuals(w, vote_floor).iter().map(|u| u.part.ratio).collect();
|
||||
p.sort_by(f64::total_cmp);
|
||||
p.dedup();
|
||||
p
|
||||
}
|
||||
|
||||
/// The quorums an author may set on a web of `w` (ADR 0013), from the lowest bound to W: every one
|
||||
/// up to W 1 000; beyond, `quorum_sample`.
|
||||
fn author_quorums(w: u32, lowest: u32, around: &[f64]) -> Vec<u32> {
|
||||
if w <= 1000 { (lowest..=w).collect() } else { quorum_sample(w, lowest, around) }
|
||||
}
|
||||
|
||||
/// Quorums from the lowest bound to W, where every one would be too many: the bounds and their
|
||||
/// neighbours, the quorums of the levels and theirs, 100, 1 000, W/2, and around each number of
|
||||
/// voters of `around` (the participation habituelle, the median).
|
||||
fn quorum_sample(w: u32, lowest: u32, around: &[f64]) -> Vec<u32> {
|
||||
let mut q = vec![lowest, 2, 3, 11, 12, 13, 49, 50, 51, 100, 1000, w / 2, w - 1, w];
|
||||
for a in around {
|
||||
let (down, up) = (a.floor() as u32, a.ceil() as u32);
|
||||
q.extend([down, up, up + 1]);
|
||||
}
|
||||
q.retain(|q| (lowest..=w).contains(q));
|
||||
q.sort_unstable();
|
||||
q.dedup();
|
||||
q
|
||||
}
|
||||
|
||||
/// One case of the grid: a web, a usual turnout of its group, a level, the quorum of the article.
|
||||
struct Case {
|
||||
w: u32,
|
||||
usual: Usual,
|
||||
k: InertiaLevel,
|
||||
q: u32,
|
||||
}
|
||||
|
||||
impl Case {
|
||||
fn at(&self) -> String {
|
||||
format!("W {} median {:?} usual {} k {} Q {}", self.w, self.usual.median, self.voters(), self.k.get(), self.q)
|
||||
}
|
||||
|
||||
/// The participation habituelle, in voters.
|
||||
fn voters(&self) -> f64 {
|
||||
self.usual.voters(self.w)
|
||||
}
|
||||
|
||||
/// The letters of the formula (the quorums of the scale do not enter: the quorum is the case's).
|
||||
fn params(&self) -> FormulaParams {
|
||||
let scale = Scale { electorate: self.w, participation: self.usual.part.ratio, quorums: [12, 12, 50, 50, 50] };
|
||||
scale.params(self.k, self.q)
|
||||
}
|
||||
|
||||
/// s_k, the share of the level.
|
||||
fn s(&self) -> f64 {
|
||||
self.k.share_pct() / 100.0
|
||||
}
|
||||
|
||||
/// M_k.
|
||||
fn m(&self) -> f64 {
|
||||
self.k.majority_pct() / 100.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The grid of the closed list (lot T3c, `docs/chantiers/acte-certification/t3c-liste-fermee.md`),
|
||||
/// on which every sentence of F1.2 and N1.1 that says a number, a share or a direction is checked
|
||||
/// against the engine: every W of the grid, from 100, the lowest the server takes as true
|
||||
/// (`wot::PLAUSIBLE`), to 10 000 000; every usual turnout of `usuals` — the reference, medians under
|
||||
/// 1 % of W, within the bounds, above 50 %, each brought back within its bounds as the engine does;
|
||||
/// the five levels; the quorums an author may set, from 2 to W (`author_quorums`).
|
||||
fn grid() -> Vec<Case> {
|
||||
let mut grid = Vec::new();
|
||||
for w in ELECTORATES {
|
||||
let (lowest, _) = quorum_bounds(w, false);
|
||||
for usual in usuals(w, 50.0) {
|
||||
let around: Vec<f64> = std::iter::once(usual.voters(w)).chain(usual.median.map(f64::from)).collect();
|
||||
for k in InertiaLevel::ALL {
|
||||
for q in author_quorums(w, lowest, &around) {
|
||||
grid.push(Case { w, usual, k, q });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
grid
|
||||
}
|
||||
|
||||
/// The share the engine asks of `voters` voters on a web of `w` — `voters` the participation
|
||||
/// habituelle, not a whole number when it is a bound (1 % of 7 224 is 72,24 voters). The engine
|
||||
/// reads T'/W only (`required_ratio`): on a web a hundred times larger, the same share is asked of a
|
||||
/// hundred times more voters, a whole number for every usual turnout of the grid. Checked against
|
||||
/// the engine on whole numbers (`the_share_of_a_level_is_asked_at_the_usual_turnout_…`).
|
||||
fn share_at(w: u32, voters: f64, params: &FormulaParams) -> f64 {
|
||||
let scaled = voters * 100.0;
|
||||
assert!((scaled - scaled.round()).abs() < 1e-6, "{voters} voters on {w}");
|
||||
required_ratio(w * 100, scaled.round() as u32, params)
|
||||
}
|
||||
|
||||
/// N1.1, « Chaque cran a aussi son quorum : sous le quorum de l'article, les votants qui manquent
|
||||
/// comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté » (lot T3; « de
|
||||
/// voix pour que » read first as « pour que » until 2ce2028; a bare « sous le quorum », after « Chaque
|
||||
/// cran a aussi son quorum », read first as the level's until e7ab82f, an article holding the one its
|
||||
/// author set). F1.2, « Q, le quorum : sous ce nombre de votants, ceux qui manquent comptent comme
|
||||
/// « non » » and « Dans Monnaie libre, jamais moins de 2 : une seule voix ne fait jamais passer un
|
||||
/// texte ». True for every quorum an author may set (2 to W in Monnaie libre, 1 to W in the test
|
||||
/// group), every level, every W, every participation of the grid (both bounds, where medians under
|
||||
/// 1 % and above 50 % of W are brought back): the « pour » that pass with nobody against, fewer
|
||||
/// voters than the quorum, are the threshold of the quorum; under it, a tally passes exactly when its
|
||||
/// « pour » reach that threshold; in Monnaie libre, one « pour » never passes, whatever the
|
||||
/// « contre ». The phrase of T2, « qui n'est pas un nombre minimal de votants », is false: at a
|
||||
/// quorum of 2, two « pour » are asked, at every level, on every W.
|
||||
#[test]
|
||||
fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
|
||||
let today = today_seed();
|
||||
let said = [
|
||||
("fr", "et il faut autant de « pour » que si le quorum avait voté."),
|
||||
("en", "and it takes as many votes in favour as if the quorum had voted."),
|
||||
("es", "y hacen falta tantos votos a favor como si hubiera votado el quórum."),
|
||||
(
|
||||
"fr",
|
||||
"sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté.",
|
||||
),
|
||||
(
|
||||
"en",
|
||||
"below the article's quorum, the voters who are missing count as votes against, and it takes as many votes in favour as if that quorum had voted.",
|
||||
),
|
||||
(
|
||||
"es",
|
||||
"por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum.",
|
||||
),
|
||||
];
|
||||
for (lang, phrase) in said {
|
||||
assert!(text(&today, "N1.1", lang).1.contains(phrase), "N1.1 {lang}: « {phrase} »");
|
||||
@@ -789,7 +1040,11 @@ fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
|
||||
for &w in electorates {
|
||||
let (lowest, highest) = quorum_bounds(w, test_group);
|
||||
assert_eq!(highest, w.max(lowest));
|
||||
for p in participations(w, vote_floor) {
|
||||
let ps = participations(w, vote_floor);
|
||||
if !test_group {
|
||||
assert!(ps.contains(&Participation::LOWEST) && ps.contains(&Participation::HIGHEST), "W {w}: {ps:?}");
|
||||
}
|
||||
for p in ps {
|
||||
let scale = Scale { electorate: w, participation: p, quorums: [lowest; 5] };
|
||||
for k in InertiaLevel::ALL {
|
||||
for q in quorums(w, lowest) {
|
||||
@@ -810,6 +1065,13 @@ fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// F1.2: in Monnaie libre, one « pour » never passes, whatever the « contre ».
|
||||
if !test_group {
|
||||
for n in [0, 1, q, w - 1] {
|
||||
let lone = Tally { for_votes: 1, against: n };
|
||||
assert!(!passes(lone, w, ¶ms).expect("valid"), "{at}: 1 pour, {n} contre");
|
||||
}
|
||||
}
|
||||
if at_quorum == q {
|
||||
minimum.push((test_group, w, k.get(), q));
|
||||
}
|
||||
@@ -830,75 +1092,136 @@ fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
|
||||
assert!(minimum.iter().any(|m| m.0 && m.3 == 1));
|
||||
}
|
||||
|
||||
/// One case of the grid of « comme d'habitude »: a web, the usual turnout of its group (in voters,
|
||||
/// and as p̃), a level and a quorum.
|
||||
struct AsUsual {
|
||||
w: u32,
|
||||
usual: u32,
|
||||
p: f64,
|
||||
k: InertiaLevel,
|
||||
q: u32,
|
||||
}
|
||||
|
||||
impl AsUsual {
|
||||
fn at(&self) -> String {
|
||||
format!("W {} usual {} k {} Q {}", self.w, self.usual, self.k.get(), self.q)
|
||||
/// N1.1, « La part exigée dépend du cran et du nombre de votants, comparé à la participation
|
||||
/// habituelle du groupe : au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la
|
||||
/// majorité M quand tout le monde vote » (lot T3c: « la part exigée », the one the formula below
|
||||
/// names; « ce quorum », the article's). On the grid: under the article's quorum, the share asked
|
||||
/// stays that of the quorum; from it, it falls with every voter more, strictly, down to M when
|
||||
/// everyone votes; and it depends on the participation habituelle (at it, s_k:
|
||||
/// `the_share_of_a_level_is_asked_at_the_usual_turnout_…`). « La part » alone could be read as the
|
||||
/// share of « pour » the voters must bring in whole votes: that one rises at times beyond the quorum
|
||||
/// — 9 « pour » of 12 voters, 75 %, then 10 of 13, 76,9 % (W 5 707, the reference turnout, level 2,
|
||||
/// quorum 12), while the share asked falls from 73,5 % to 73,2 %.
|
||||
#[test]
|
||||
fn the_share_asked_falls_beyond_the_article_s_quorum_down_to_m() {
|
||||
let today = today_seed();
|
||||
for (lang, phrase) in [
|
||||
(
|
||||
"fr",
|
||||
"La part exigée dépend du cran et du nombre de votants, comparé à la participation habituelle du groupe : au-delà de ce quorum, plus il y a de votants, plus elle baisse, jusqu'à la majorité M quand tout le monde vote.",
|
||||
),
|
||||
(
|
||||
"en",
|
||||
"The share asked depends on the level and on the number of voters, compared with the group's usual turnout: beyond that quorum, the more voters, the lower it gets, down to the majority M when everyone votes.",
|
||||
),
|
||||
(
|
||||
"es",
|
||||
"La proporción exigida depende del nivel y del número de votantes, comparado con la participación habitual del grupo: más allá de ese quórum, cuantos más votantes, más baja, hasta la mayoría M cuando vota todo el mundo.",
|
||||
),
|
||||
] {
|
||||
assert!(text(&today, "N1.1", lang).1.contains(phrase), "N1.1 {lang}: « {phrase} »");
|
||||
}
|
||||
}
|
||||
|
||||
/// The grid of « comme d'habitude » (lot T3 and its review): every W of the grid, usual turnouts
|
||||
/// within the bounds (a median out of them is brought back, and is then not the group's usual
|
||||
/// turnout), every level, quorums from 2 to W, among them the usual turnout and one more.
|
||||
fn as_usual_grid() -> Vec<AsUsual> {
|
||||
let mut grid = Vec::new();
|
||||
for w in ELECTORATES {
|
||||
for usual in [w / 100 + 1, 12, 40, 50, 100, 120, 1000, w / 2] {
|
||||
let part = Participation::of(w, 50.0, Some(f64::from(usual)), 9);
|
||||
if part.bounded {
|
||||
continue;
|
||||
}
|
||||
for k in InertiaLevel::ALL {
|
||||
for q in [2, 12, 50, 100, 1000, usual, usual + 1, w].into_iter().filter(|q| (2..=w).contains(q)) {
|
||||
grid.push(AsUsual { w, usual, p: part.ratio, k, q });
|
||||
}
|
||||
let (mut under, mut beyond) = (0u64, 0u64);
|
||||
for c in grid() {
|
||||
let p = c.params();
|
||||
let share = |t: u32| required_ratio(c.w, judged_voters(t, c.q), &p);
|
||||
let at = c.at();
|
||||
// Every number of voters on the smallest webs; beyond, around the quorum, the participation
|
||||
// habituelle and the median, W/2, W.
|
||||
let mut ts: Vec<u32> = if c.w <= 101 {
|
||||
(0..=c.w).collect()
|
||||
} else {
|
||||
let usual = c.voters();
|
||||
let mut ts = vec![0, 1, c.q - 1, c.q, c.q + 1, c.q + 2, 2 * c.q, usual.floor() as u32, usual.ceil() as u32];
|
||||
ts.extend(c.usual.median);
|
||||
ts.extend([c.w / 2, c.w - 1, c.w]);
|
||||
ts
|
||||
};
|
||||
ts.retain(|t| *t <= c.w);
|
||||
ts.sort_unstable();
|
||||
ts.dedup();
|
||||
for pair in ts.windows(2) {
|
||||
let (a, b) = (pair[0], pair[1]);
|
||||
if a >= c.q {
|
||||
assert!(share(b) < share(a), "{at}: from {a} to {b} voters, {} then {}", share(a), share(b));
|
||||
beyond += 1;
|
||||
} else if b <= c.q {
|
||||
assert_eq!(share(a), share(b), "{at}: under the quorum, {a} and {b} voters");
|
||||
under += 1;
|
||||
}
|
||||
}
|
||||
assert!((share(c.w) - c.m()).abs() < 1e-12, "{at}: M when everyone votes");
|
||||
}
|
||||
grid
|
||||
assert!(under > 0 && beyond > 0, "under {under}, beyond {beyond}");
|
||||
// The share of « pour » in whole votes is not the share asked: it rises from 12 to 13 voters.
|
||||
let w = 5707;
|
||||
let scale = Scale { electorate: w, participation: Participation::of(w, 50.0, None, 0).ratio, quorums: [12; 5] };
|
||||
let p = scale.params(level(2), 12);
|
||||
let (at12, at13) = (wot_threshold(w, 12, &p).expect("valid"), wot_threshold(w, 13, &p).expect("valid"));
|
||||
assert_eq!((at12, at13), (9, 10));
|
||||
assert!(f64::from(at13) / 13.0 > f64::from(at12) / 12.0);
|
||||
assert!(required_ratio(w, 13, &p) < required_ratio(w, 12, &p));
|
||||
}
|
||||
|
||||
/// The letters of the formula for a case of the grid (the quorums of the scale do not enter: the
|
||||
/// quorum is the case's).
|
||||
fn as_usual_params(c: &AsUsual) -> FormulaParams {
|
||||
Scale { electorate: c.w, participation: c.p, quorums: [12, 12, 50, 50, 50] }.params(c.k, c.q)
|
||||
}
|
||||
|
||||
/// N1.1 and F1.2, lot T3: the share of a level « comme d'habitude » is asked beyond the quorum
|
||||
/// only. When the usual turnout reaches the quorum, the group voting as usual is asked s_k; when
|
||||
/// it does not (a web of fewer than 5 000 members and a median under 50 at a quorum of 50, an
|
||||
/// author's quorum above the usual turnout), it is asked the threshold of the quorum, which is
|
||||
/// not s_k of those who came. Every W, every median within the bounds, every level, quorums.
|
||||
/// N1.1, « Les cinq crans : le quorum dans Monnaie libre, la part exigée au-delà du quorum de
|
||||
/// l'article, à la participation habituelle, puis quand tout le monde vote » and its five lines
|
||||
/// (« 60 % à la participation habituelle, 50 % quand tout le monde vote »…); F1.2, « au-delà du
|
||||
/// quorum, le groupe demande 75 % de « pour » à sa participation habituelle ». Lot T3 wrote « comme
|
||||
/// d'habitude », which reads first as the real median: false where the median leaves its bounds
|
||||
/// (`the_participation_habituelle_is_the_median_brought_back_within_its_bounds`); the defined term
|
||||
/// since the review of e7ab82f. On the grid: when the participation habituelle reaches the article's
|
||||
/// quorum, the share asked there is s_k exactly; when it does not (a quorum above it: the author's,
|
||||
/// or the level's on a small web), the voters are judged as the quorum, at a share lower than s_k,
|
||||
/// asked of more voters than came; when everyone votes, M_k, at every quorum.
|
||||
#[test]
|
||||
fn the_share_of_a_level_as_usual_is_asked_beyond_the_quorum() {
|
||||
let mut under = 0;
|
||||
for c in as_usual_grid() {
|
||||
let (s, m) = (c.k.share_pct() / 100.0, c.k.majority_pct() / 100.0);
|
||||
let params = as_usual_params(&c);
|
||||
fn the_share_of_a_level_is_asked_at_the_usual_turnout_beyond_the_article_s_quorum() {
|
||||
let today = today_seed();
|
||||
for (lang, intro) in [
|
||||
(
|
||||
"fr",
|
||||
"la part exigée au-delà du quorum de l'article, à la participation habituelle, puis quand tout le monde vote",
|
||||
),
|
||||
("en", "the share asked beyond the article's quorum, at the usual turnout, then when everyone votes"),
|
||||
(
|
||||
"es",
|
||||
"la proporción exigida más allá del quórum del artículo, con la participación habitual, luego cuando vota todo el mundo",
|
||||
),
|
||||
] {
|
||||
assert!(text(&today, "N1.1", lang).1.contains(intro), "N1.1 {lang}: « {intro} »");
|
||||
}
|
||||
// The five lines and F1.2 carry their numbers: `every_number_of_the_quorum_texts_is_the_engine_s`.
|
||||
let (mut beyond, mut under, mut low, mut high) = (0u64, 0u64, 0u64, 0u64);
|
||||
for c in grid() {
|
||||
let (s, m, p) = (c.s(), c.m(), c.params());
|
||||
let at = c.at();
|
||||
let asked = required_ratio(c.w, judged_voters(c.usual, c.q), ¶ms);
|
||||
if c.usual >= c.q {
|
||||
let usual = c.voters();
|
||||
if (usual - usual.round()).abs() < 1e-9 {
|
||||
let whole = required_ratio(c.w, usual.round() as u32, &p);
|
||||
assert!(close(share_at(c.w, usual, &p), whole), "{at}: the share on a web a hundred times larger");
|
||||
}
|
||||
let asked = share_at(c.w, usual.max(f64::from(c.q)), &p);
|
||||
if usual >= f64::from(c.q) {
|
||||
assert!((asked - s).abs() < 1e-9, "{at}: {asked}, not s");
|
||||
beyond += 1;
|
||||
} else {
|
||||
assert_eq!(asked, required_ratio(c.w, c.q, &p), "{at}: judged as the quorum");
|
||||
assert!(asked < s - 1e-9, "{at}: {asked}");
|
||||
under += 1;
|
||||
}
|
||||
if c.usual.part.bounded && usual >= f64::from(c.q) {
|
||||
if c.usual.part.ratio == Participation::LOWEST {
|
||||
low += 1;
|
||||
} else {
|
||||
high += 1;
|
||||
}
|
||||
}
|
||||
// Everyone votes: M, at every quorum.
|
||||
assert!((required_ratio(c.w, judged_voters(c.w, c.q), ¶ms) - m).abs() < 1e-12, "{at}: M");
|
||||
assert!((required_ratio(c.w, judged_voters(c.w, c.q), &p) - m).abs() < 1e-12, "{at}: M");
|
||||
}
|
||||
assert!(under > 0);
|
||||
assert!(beyond > 0 && under > 0 && low > 0 && high > 0, "beyond {beyond}, under {under}, low {low}, high {high}");
|
||||
// The case of F1.2, its default level and quorum: W 3 000, a usual turnout of 40 voters
|
||||
// (within the bounds: 1 % is 30). Voting as usual, the group is asked 38 « pour » of its 40
|
||||
// voters: 95 %, not 75 %.
|
||||
// (within the bounds: 1 % is 30). At its participation habituelle, the group is asked 38 « pour »
|
||||
// of its 40 voters: 95 %, not 75 % — under the quorum, which the sentence leaves out.
|
||||
let (w, usual) = (3000, 40);
|
||||
let part = Participation::of(w, 50.0, Some(f64::from(usual)), 9);
|
||||
assert!(!part.bounded);
|
||||
@@ -912,19 +1235,20 @@ fn the_share_of_a_level_as_usual_is_asked_beyond_the_quorum() {
|
||||
assert_eq!(wot_threshold(w, usual, &p), Ok(38), "but 40 voters are judged as 50");
|
||||
}
|
||||
|
||||
/// N1.1, the sentence of G, after the review of 2ce2028. Lot T3 left « G se calcule pour que la
|
||||
/// part exigée, à la participation habituelle, soit exactement celle du cran […], où s est la part
|
||||
/// exigée comme d'habitude »: false where the usual turnout is under the quorum, the group voting
|
||||
/// as usual being judged on the quorum (T' = Q), lower on the curve than s, and asked of more
|
||||
/// voters than came. Written true for every W and every quorum: G is computed so that the share
|
||||
/// asked, beyond the quorum, is exactly that of the level when the group votes as usual; s is that
|
||||
/// share of the level, the one of the list (60, 67, 75, 83 or 90 %) — never « la part exigée »,
|
||||
/// which the formula above it names and which moves with T'. On the grid of « comme d'habitude »,
|
||||
/// with G as the text writes it.
|
||||
/// N1.1, the sentence of G: « G se calcule pour que la part exigée, au-delà du quorum de l'article,
|
||||
/// soit exactement celle du cran à la participation habituelle : `G = …`, où s est cette part du
|
||||
/// cran (60, 67, 75, 83 ou 90 %) et p̃ la participation habituelle divisée par W » — after the
|
||||
/// reviews of 2ce2028 (« au-delà du quorum »: under it, the voters are judged as the quorum) and of
|
||||
/// e7ab82f (the defined term where T3b wrote « quand le groupe vote comme d'habitude »; « du quorum
|
||||
/// de l'article »). s is the share of the level, the one of the list — never « la part exigée »,
|
||||
/// which the formula above names and which moves with T'. On the grid, with G as the text writes
|
||||
/// it and p̃ the participation habituelle over W, the median brought back within its bounds: G is
|
||||
/// the engine's, at every quorum; on the curve, the share at the participation habituelle is s; the
|
||||
/// share asked there is s beyond the article's quorum, and lower, judged as the quorum, under it.
|
||||
#[test]
|
||||
fn g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usual() {
|
||||
fn g_gives_the_share_of_the_level_at_the_usual_turnout_beyond_the_article_s_quorum() {
|
||||
let today = today_seed();
|
||||
// The sentence of G closes N1.1, in each language; nothing of the sentence of T3 is left.
|
||||
// The sentence of G closes N1.1, in each language; nothing of the sentence of T2 is left.
|
||||
for lang in LANGS {
|
||||
let (_, body) = text(&today, "N1.1", lang);
|
||||
let (_, _, before, now) = PHRASES_T3
|
||||
@@ -933,44 +1257,50 @@ fn g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usua
|
||||
.expect("the sentence of G");
|
||||
assert!(body.ends_with(now), "N1.1 {lang}: the sentence of G\n{body}");
|
||||
let (said_before, _) = before.split_once(" = ").expect("the formula");
|
||||
assert!(!body.contains(said_before), "N1.1 {lang}: the sentence of T3 is left\n{body}");
|
||||
let beyond = named(lang, "au-delà du quorum", "beyond the quorum", "más allá del quórum");
|
||||
assert!(!body.contains(said_before), "N1.1 {lang}: the sentence of T2 is left\n{body}");
|
||||
let (said_now, _) = now.split_once(" = ").expect("the formula");
|
||||
assert!(said_now.contains(beyond), "N1.1 {lang}: « {beyond} »");
|
||||
for words in [
|
||||
named(
|
||||
lang,
|
||||
"au-delà du quorum de l'article",
|
||||
"beyond the article's quorum",
|
||||
"más allá del quórum del artículo",
|
||||
),
|
||||
named(lang, "à la participation habituelle", "at the usual turnout", "con la participación habitual"),
|
||||
] {
|
||||
assert!(said_now.contains(words), "N1.1 {lang}: « {words} »");
|
||||
}
|
||||
}
|
||||
// By the engine, with G as the text writes it (s, the share of the level; M; p̃). On the curve,
|
||||
// the share at the usual turnout is s, whatever the quorum: that is what G is computed for.
|
||||
// The share asked of the group voting as usual is s beyond the quorum; under it, the share of
|
||||
// the quorum, lower — the sentence of T3 said s there.
|
||||
// By the engine, with G as the text writes it (s, the share of the level; M; p̃).
|
||||
let grad = code_spans(text(&today, "N1.1", "fr").1)[1];
|
||||
let (mut beyond, mut under) = (0, 0);
|
||||
for c in as_usual_grid() {
|
||||
let (s, m) = (c.k.share_pct() / 100.0, c.k.majority_pct() / 100.0);
|
||||
let env: HashMap<String, f64> = HashMap::from([("s".into(), s), ("M".into(), m), ("p̃".into(), c.p)]);
|
||||
let engine = as_usual_params(&c);
|
||||
let (mut beyond, mut under) = (0u64, 0u64);
|
||||
for c in grid() {
|
||||
let (s, m) = (c.s(), c.m());
|
||||
let env: HashMap<String, f64> =
|
||||
HashMap::from([("s".into(), s), ("M".into(), m), ("p̃".into(), c.usual.part.ratio)]);
|
||||
let engine = c.params();
|
||||
let params = FormulaParams { gradient: eval(grad, "G", &env), ..engine };
|
||||
let at = c.at();
|
||||
assert!(close(params.gradient, engine.gradient), "{at}: G");
|
||||
assert!((required_ratio(c.w, c.usual, ¶ms) - s).abs() < 1e-9, "{at}: s on the curve");
|
||||
let judged = judged_voters(c.usual, c.q);
|
||||
let asked = required_ratio(c.w, judged, ¶ms);
|
||||
if c.usual >= c.q {
|
||||
assert_eq!(judged, c.usual, "{at}");
|
||||
let usual = c.voters();
|
||||
assert!((share_at(c.w, usual, ¶ms) - s).abs() < 1e-9, "{at}: s on the curve");
|
||||
let asked = share_at(c.w, usual.max(f64::from(c.q)), ¶ms);
|
||||
if usual >= f64::from(c.q) {
|
||||
assert!((asked - s).abs() < 1e-9, "{at}: {asked}, not s");
|
||||
beyond += 1;
|
||||
} else {
|
||||
assert_eq!(judged, c.q, "{at}");
|
||||
assert!(asked < s - 1e-9, "{at}: {asked}");
|
||||
under += 1;
|
||||
}
|
||||
}
|
||||
assert!(beyond > 0 && under > 0, "beyond {beyond}, under {under}");
|
||||
// The case of the review: W 3 000, a usual turnout of 40 voters, level 3, quorum 50. On the
|
||||
// curve, 75 % at 40 voters; the share asked is that of 50 voters, 74,23 %; 38 « pour » of the
|
||||
// 40 who came, 95 %.
|
||||
// The case of the review of 2ce2028: W 3 000, a usual turnout of 40 voters, level 3, quorum 50.
|
||||
// On the curve, 75 % at 40 voters; the share asked is that of 50 voters, 74,23 %; 38 « pour »
|
||||
// of the 40 who came, 95 %.
|
||||
let (w, usual) = (3000, 40);
|
||||
let c = AsUsual { w, usual, p: Participation::of(w, 50.0, Some(f64::from(usual)), 9).ratio, k: level(3), q: 50 };
|
||||
let p = as_usual_params(&c);
|
||||
let part = Participation::of(w, 50.0, Some(f64::from(usual)), 9);
|
||||
let c = Case { w, usual: Usual { median: Some(usual), part }, k: level(3), q: 50 };
|
||||
let p = c.params();
|
||||
assert!((required_ratio(w, usual, &p) - 0.75).abs() < 1e-12);
|
||||
let asked = required_ratio(w, judged_voters(usual, c.q), &p);
|
||||
assert_eq!((asked * 10_000.0).round() / 100.0, 74.23);
|
||||
@@ -978,8 +1308,113 @@ fn g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usua
|
||||
assert_eq!((needed, f64::from(needed) / f64::from(usual)), (38, 0.95));
|
||||
}
|
||||
|
||||
/// N1.1, « Tant que le groupe n'a pas voté sur 5 propositions, elle vaut deux fois le quorum de
|
||||
/// référence du document : 100 votants dans Monnaie libre. […] Elle reste entre 1 % et 50 % de
|
||||
/// N1.1, « La participation habituelle est la médiane du nombre de votants des propositions du
|
||||
/// groupe votées depuis un an. Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois
|
||||
/// le quorum de référence du document […]. Elle est ramenée entre 1 % et 50 % de l'électorat » —
|
||||
/// and why F1.2 and N1.1 write this defined term where lot T3 wrote « comme d'habitude » (review of
|
||||
/// e7ab82f). « Quand le groupe vote comme d'habitude » reads first as « when as many vote as the
|
||||
/// median »: the engine computes G on the median brought back within its bounds, and where the
|
||||
/// median is out of them, the share asked of as many voters as the median is not s_k — higher under
|
||||
/// 1 % of W, lower above 50 %. The case of the review: W 7 224, a median of 40 voters (0,55 %),
|
||||
/// level 2, quorum 12 — at 40 voters the share asked is 68,7 %, 28 « pour » of 40, not 27 (67 %);
|
||||
/// at the participation habituelle, 72,24 voters (1 % of W), 67 % exactly. « Tant que le groupe n'a
|
||||
/// pas voté sur 5 propositions » (T2) was false once the group had: the median rests on the
|
||||
/// proposals voted on within the year, and with fewer than 5 there the reference stands in again
|
||||
/// (`tests/inertia.rs`, `the_usual_participation_is_the_median_of_five_voted_proposals`); « Elle
|
||||
/// reste entre » read as a fact, not as the rule that brings the median back.
|
||||
#[test]
|
||||
fn the_participation_habituelle_is_the_median_brought_back_within_its_bounds() {
|
||||
let today = today_seed();
|
||||
// The texts write the defined term, never the paraphrase: in N1.1, the list, its five lines and
|
||||
// G (and, in French and Spanish, « comparé à la participation habituelle du groupe », already
|
||||
// there); in F1.2, « Par défaut ».
|
||||
for (lang, paraphrase, term, times, its, rests, bounds) in [
|
||||
(
|
||||
"fr",
|
||||
"comme d'habitude",
|
||||
"à la participation habituelle",
|
||||
8,
|
||||
"à sa participation habituelle",
|
||||
"Tant qu'elle repose sur moins de",
|
||||
"Elle est ramenée entre",
|
||||
),
|
||||
(
|
||||
"en",
|
||||
"as usual",
|
||||
"at the usual turnout",
|
||||
7,
|
||||
"at its usual turnout",
|
||||
"While it rests on fewer than",
|
||||
"It is kept between",
|
||||
),
|
||||
(
|
||||
"es",
|
||||
"como de costumbre",
|
||||
"con la participación habitual",
|
||||
8,
|
||||
"con su participación habitual",
|
||||
"Mientras se base en menos de",
|
||||
"Se acota entre",
|
||||
),
|
||||
] {
|
||||
let (n11, f12) = (text(&today, "N1.1", lang).1, text(&today, "F1.2", lang).1);
|
||||
for (code, body) in [("N1.1", n11), ("F1.2", f12)] {
|
||||
assert!(!body.contains(paraphrase), "{code} {lang}: « {paraphrase} »\n{body}");
|
||||
}
|
||||
assert_eq!(n11.matches(term).count(), times, "N1.1 {lang}: the list, its five lines, G\n{n11}");
|
||||
assert_eq!(f12.matches(its).count(), 1, "F1.2 {lang}: by default");
|
||||
assert!(n11.contains(rests) && n11.contains(bounds), "N1.1 {lang}: the definition");
|
||||
}
|
||||
// The engine: the median — or, on fewer than 5 proposals, the reference — brought back within
|
||||
// 1 % and 50 % of W.
|
||||
for w in ELECTORATES {
|
||||
for u in usuals(w, 50.0) {
|
||||
let raw = match u.median {
|
||||
Some(m) => f64::from(m) / f64::from(w),
|
||||
None => Participation::reference_ratio(w, 50.0),
|
||||
};
|
||||
let at = format!("W {w} {u:?}");
|
||||
assert_eq!(u.part.ratio, raw.clamp(Participation::LOWEST, Participation::HIGHEST), "{at}");
|
||||
assert_eq!(u.part.bounded, u.part.ratio != raw, "{at}");
|
||||
if let Some(m) = u.median {
|
||||
let few = Participation::of(w, 50.0, Some(f64::from(m)), Participation::MIN_SAMPLE - 1);
|
||||
assert_eq!(few.ratio, Participation::of(w, 50.0, None, 0).ratio, "{at}: the reference");
|
||||
}
|
||||
}
|
||||
}
|
||||
// The case of the review.
|
||||
let (w, median) = (7224, 40);
|
||||
let part = Participation::of(w, 50.0, Some(f64::from(median)), 9);
|
||||
assert_eq!((part.bounded, part.ratio), (true, Participation::LOWEST));
|
||||
let c = Case { w, usual: Usual { median: Some(median), part }, k: level(2), q: 12 };
|
||||
let p = c.params();
|
||||
assert_eq!((required_ratio(w, median, &p) * 1000.0).round() / 10.0, 68.7);
|
||||
assert_eq!(wot_threshold(w, median, &p), Ok(28));
|
||||
assert_eq!((c.s() * f64::from(median)).ceil(), 27.0, "67 % of 40, rounded up");
|
||||
assert!(!passes(Tally { for_votes: 27, against: 13 }, w, &p).expect("valid"));
|
||||
assert!(passes(Tally { for_votes: 28, against: 12 }, w, &p).expect("valid"));
|
||||
assert!((c.voters() - 72.24).abs() < 1e-9);
|
||||
assert!((share_at(w, c.voters(), &p) - c.s()).abs() < 1e-12, "67 % at the participation habituelle");
|
||||
// On the grid: every median out of its bounds that reaches the quorum — as many voters as the
|
||||
// median are not asked s_k.
|
||||
let (mut under_one, mut above_half) = (0u64, 0u64);
|
||||
for c in grid() {
|
||||
let Some(median) = c.usual.median.filter(|m| c.usual.part.bounded && *m >= c.q) else { continue };
|
||||
let at = c.at();
|
||||
let at_median = required_ratio(c.w, median, &c.params());
|
||||
if c.usual.part.ratio == Participation::LOWEST {
|
||||
assert!(at_median > c.s() + 1e-9, "{at}: {at_median}");
|
||||
under_one += 1;
|
||||
} else {
|
||||
assert!(at_median < c.s() - 1e-9, "{at}: {at_median}");
|
||||
above_half += 1;
|
||||
}
|
||||
}
|
||||
assert!(under_one > 0 && above_half > 0, "under 1 % {under_one}, above 50 % {above_half}");
|
||||
}
|
||||
|
||||
/// N1.1, « Tant qu'elle repose sur moins de 5 propositions, elle vaut deux fois le quorum de
|
||||
/// référence du document : 100 votants dans Monnaie libre. […] Elle est ramenée entre 1 % et 50 % de
|
||||
/// l'électorat » (review of 2ce2028). 100 is a parameter, 2 × voteFloor, which the number test
|
||||
/// checks as such (`figures`); it is not a turnout that holds on every W: the engine's p0 is 100
|
||||
/// voters on a web of 200 to 10 000 members only (1 % and 50 % of W); beyond, the bounds the
|
||||
@@ -1002,9 +1437,9 @@ fn the_reference_turnout_is_a_parameter_the_bounds_bring_back() {
|
||||
}
|
||||
// The text says the parameter first, then the bounds, in each language.
|
||||
for (lang, reference, bounds) in [
|
||||
("fr", format!("{} votants dans Monnaie libre.", f.reference), "Elle reste entre"),
|
||||
("en", format!("{} voters in Monnaie libre.", f.reference), "It stays between"),
|
||||
("es", format!("{} votantes en Monnaie libre.", f.reference), "Se mantiene entre"),
|
||||
("fr", format!("{} votants dans Monnaie libre.", f.reference), "Elle est ramenée entre"),
|
||||
("en", format!("{} voters in Monnaie libre.", f.reference), "It is kept between"),
|
||||
("es", format!("{} votantes en Monnaie libre.", f.reference), "Se acota entre"),
|
||||
] {
|
||||
let (_, body) = text(&seed, "N1.1", lang);
|
||||
let at = |s: &str| body.find(s).unwrap_or_else(|| panic!("N1.1 {lang}: « {s} »"));
|
||||
@@ -1017,7 +1452,9 @@ fn the_reference_turnout_is_a_parameter_the_bounds_bring_back() {
|
||||
/// web of trust (`wot::PLAUSIBLE`, 100 to 10 000 000) — the engine's threshold is the one
|
||||
/// without B, even where the share lands on a whole number (`threshold::settle`, `clamp_ceil`):
|
||||
/// M × W when everyone votes, s × the usual turnout. Where W is a handful of members (the test
|
||||
/// group, whose W is not the web of trust these texts speak of), B^W does count.
|
||||
/// group, whose W is not the web of trust these texts speak of), B^W does count. Lot T3c: on the
|
||||
/// participations of the grid (both bounds, where medians under 1 % and above 50 % of W are brought
|
||||
/// back) and quorums from 2 to W (`quorum_sample`).
|
||||
#[test]
|
||||
fn b_to_the_power_w_never_counts_in_the_web_of_trust() {
|
||||
assert_eq!((*PLAUSIBLE.start(), *PLAUSIBLE.end()), (ELECTORATES[0], ELECTORATES[ELECTORATES.len() - 1]));
|
||||
@@ -1027,7 +1464,7 @@ fn b_to_the_power_w_never_counts_in_the_web_of_trust() {
|
||||
let usual = (p * f64::from(w)).round() as u32;
|
||||
let scale = Scale { electorate: w, participation: p, quorums: [12, 12, 50, 50, 50] };
|
||||
for k in InertiaLevel::ALL {
|
||||
for q in [2, 12, 50, 100, w].into_iter().filter(|q| *q <= w) {
|
||||
for q in quorum_sample(w, 2, &[p * f64::from(w)]) {
|
||||
let with_b = scale.params(k, q);
|
||||
assert_eq!(with_b.base, 0.1);
|
||||
let without = FormulaParams { base: 0.0, ..with_b };
|
||||
@@ -1199,14 +1636,22 @@ fn the_formulas_of_the_texts_are_those_the_engine_computes() {
|
||||
let [voters, threshold] = <[&str; 2]>::try_from(code_spans(f11)).expect("two formulas in F1.1");
|
||||
let [share, grad] = <[&str; 2]>::try_from(code_spans(n11)).expect("two formulas in N1.1");
|
||||
let mut checked = 0;
|
||||
for w in [2u32, 40, 5707, 7224] {
|
||||
for median in [None, Some(1.0), Some(9.0), Some(120.0), Some(360.0)] {
|
||||
let p = Participation::of(w, 50.0, median, 9).ratio;
|
||||
// Lot T3c: every W of the grid, and two of a handful of members; every participation of the
|
||||
// grid (both bounds among them, where medians under 1 % and above 50 % of W are brought
|
||||
// back: p̃ is the participation habituelle over W); quorums from 1 to W.
|
||||
for w in [2u32, 40].into_iter().chain(ELECTORATES) {
|
||||
for p in participations(w, 50.0) {
|
||||
let scale = Scale { electorate: w, participation: p, quorums: [12, 12, 50, 50, 50] };
|
||||
let usual = p * f64::from(w);
|
||||
let (down, up) = (usual.floor() as u32, usual.ceil() as u32);
|
||||
let mut quorums = vec![1u32, 2, 12, 50, up, w / 2, w];
|
||||
quorums.retain(|q| (1..=w).contains(q));
|
||||
quorums.sort_unstable();
|
||||
quorums.dedup();
|
||||
for k in InertiaLevel::ALL {
|
||||
for q in [1u32, 2, 12, 50].into_iter().filter(|q| *q <= w) {
|
||||
for &q in &quorums {
|
||||
let params = scale.params(k, q);
|
||||
for t in [0u32, 1, 9, 12, 13, 50, 51, 120, 500, w].into_iter().filter(|t| *t <= w) {
|
||||
for t in [0u32, 1, 9, 12, 13, 50, 51, 120, 500, down, up, w].into_iter().filter(|t| *t <= w) {
|
||||
let mut env: HashMap<String, f64> = HashMap::from([
|
||||
("T".into(), f64::from(t)),
|
||||
("Q".into(), f64::from(q)),
|
||||
|
||||
Reference in New Issue
Block a user