Lot T3b : les tests d'abord, rouges sur le texte de T3 (2ce2028) — la phrase de G de N1.1 vraie pour tout W et tout quorum, sur la grille de « comme d'habitude » (as_usual_grid, partagée avec the_share_of_a_level_as_usual_is_asked_beyond_the_quorum) ; « autant de « pour » » en français ; le test des nombres honnête sur « 100 votants » (un paramètre, 2 × voteFloor ; la participation de référence ne vaut 100 votants qu'entre 200 et 10 000 membres, un test à part)

Vus rouges sur le texte de T3 : every_number_of_the_quorum_texts_is_the_engine_s (les cinq parts
de la phrase de G manquent), g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usual,
the_texts_of_lot_t3_change_these_phrases_only, under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted.
the_reference_turnout_is_a_parameter_the_bounds_bring_back décrit le moteur : vert d'emblée, et
vu tomber quand on lui fait dire « de 100 à 10 000 membres » (W 100).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 18:28:43 +01:00
co-authored by Claude Opus 5.5
parent c05319dc4a
commit 1fe31f567b
+202 -32
View File
@@ -21,6 +21,12 @@
//! only (N1.1, F1.2). A third guarded amendment, which follows lot T2's, writes them on a base //! only (N1.1, F1.2). A third guarded amendment, which follows lot T2's, writes them on a base
//! made as production is after M6c; the texts of T2 are kept in //! made as production is after M6c; the texts of T2 are kept in
//! `tests/fixtures/seed-quorum-t2.json`. F1.1 needs none: it is not touched. //! `tests/fixtures/seed-quorum-t2.json`. F1.1 needs none: it is not touched.
//!
//! After the review of lot T3 (2ce2028, lot T3b, before it went online: the same amendment, its
//! fingerprint `to` changed): N1.1 says G gives the share of the level beyond the quorum, when the
//! 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.
use ld_engine::inertia::{InertiaLevel, Participation, Scale, gradient, quorum_bounds}; 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}; use ld_engine::threshold::{FormulaParams, Tally, judged_voters, passes, required_ratio, wot_threshold, yes_needed};
@@ -122,14 +128,18 @@ fn seed_dir(tag: &str, seed: &Value) -> PathBuf {
/// Every figure the texts may hold: parameters of the seed and constants of the engine and /// Every figure the texts may hold: parameters of the seed and constants of the engine and
/// the server. Since lot T3 (Yvv, 03/11: « pas de chiffre datés dans un texte voté »), none /// the server. Since lot T3 (Yvv, 03/11: « pas de chiffre datés dans un texte voté »), none
/// depends on the web of trust W or on a turnout: no participation goes in, and the same /// is computed from the web of trust W or from a turnout: no participation goes in. The one
/// figures come out on every W of Monnaie libre (`figures`). /// the engine computes from W, the lowest quorum an author may set, comes out the same on
/// every W of Monnaie libre (`figures`); the others no W enters.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
struct Figures { struct Figures {
/// Q_k of Monnaie libre (the seed). /// Q_k of Monnaie libre (the seed).
quorums: [u32; 5], quorums: [u32; 5],
/// Twice the reference quorum of the document, in voters (the engine's p0 · W, before /// « deux fois le quorum de référence du document : 100 votants » — a parameter, 2 ×
/// the bounds). /// voteFloor, written before the bounds; not a turnout that holds on every W. The engine's
/// p0 = 2 × voteFloor / W is that many voters only on a web of 200 to 10 000 members;
/// beyond, the bounds N1.1 says after it, in the same paragraph, bring it back
/// (`the_reference_turnout_is_a_parameter_the_bounds_bring_back`).
reference: u32, reference: u32,
/// The lowest quorum an author may set in Monnaie libre. /// The lowest quorum an author may set in Monnaie libre.
lowest_quorum: u32, lowest_quorum: u32,
@@ -155,8 +165,8 @@ fn figures_at(seed: &Value, w: u32) -> Figures {
let ml = let ml =
seed["instances"].as_array().expect("instances").iter().find(|i| i["group"] == "monnaie-libre").expect("ml"); seed["instances"].as_array().expect("instances").iter().find(|i| i["group"] == "monnaie-libre").expect("ml");
let quorums: [u32; 5] = serde_json::from_value(ml["quorums"].clone()).expect("quorums"); let quorums: [u32; 5] = serde_json::from_value(ml["quorums"].clone()).expect("quorums");
let vote_floor = ml["voteFloor"].as_f64().expect("voteFloor"); // Twice the reference quorum: a parameter, the same whatever W (no W enters it).
let reference = Participation::reference_ratio(w, vote_floor) * f64::from(w); let reference = 2.0 * ml["voteFloor"].as_f64().expect("voteFloor");
assert!((reference - reference.round()).abs() < 1e-6, "a whole number of voters: {reference}"); assert!((reference - reference.round()).abs() < 1e-6, "a whole number of voters: {reference}");
let levels: Vec<u8> = seed["units"] let levels: Vec<u8> = seed["units"]
.as_array() .as_array()
@@ -185,7 +195,9 @@ fn figures_at(seed: &Value, w: u32) -> Figures {
} }
} }
/// The figures of the texts: the same on every W Monnaie libre may have — none is dated. /// The figures of the texts, on every W Monnaie libre may have: none is dated. Only the lowest
/// quorum is computed from W here; the reference turnout is a parameter, and where it is a
/// turnout is `the_reference_turnout_is_a_parameter_the_bounds_bring_back`.
fn figures(seed: &Value) -> Figures { fn figures(seed: &Value) -> Figures {
let mut all = ELECTORATES.iter().map(|w| (*w, figures_at(seed, *w))); let mut all = ELECTORATES.iter().map(|w| (*w, figures_at(seed, *w)));
let (_, first) = all.next().expect("an electorate"); let (_, first) = all.next().expect("an electorate");
@@ -397,21 +409,28 @@ fn claims(code: &str, lang: &str, f: &Figures, seed: &Value) -> Vec<String> {
.collect(); .collect();
let (b0, b1) = (pct(lang, f.bounds.0), pct(lang, f.bounds.1)); let (b0, b1) = (pct(lang, f.bounds.0), pct(lang, f.bounds.1));
let (ms, r) = (f.min_sample, f.reference); let (ms, r) = (f.min_sample, f.reference);
// s of each level, as the sentence of G names them: the shares of the list, level 1 to 5.
let (top, lower) = InertiaLevel::ALL.split_last().expect("five levels");
let head = lower.iter().map(|k| dec(lang, k.share_pct())).collect::<Vec<_>>().join(", ");
let last = pct(lang, top.share_pct());
out.extend(match lang { out.extend(match lang {
"fr" => [ "fr" => [
"propositions du groupe votées depuis un an".to_owned(), "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!("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!("Elle reste 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" => [ "en" => [
"the group's proposals voted on over the past year".to_owned(), "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!("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!("It stays 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(), "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!("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!("Se mantiene 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})"),
], ],
}); });
out out
@@ -600,9 +619,10 @@ 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 // 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. F1.1 /// (article, language, the words of T2, the words of today): the only change of lot T3, with its
/// has none: it holds no dated figure and every sentence of it is true (`b_to_the_power_w_…`). /// review (2ce2028: the sentence of G; « autant de « pour » » in French). F1.1 has none: it holds
const PHRASES_T3: [(&str, &str, &str, &str); 12] = [ /// no dated figure and every sentence of it is true (`b_to_the_power_w_…`).
const PHRASES_T3: [(&str, &str, &str, &str); 15] = [
( (
"F1.2", "F1.2",
"fr", "fr",
@@ -625,7 +645,7 @@ const PHRASES_T3: [(&str, &str, &str, &str); 12] = [
"N1.1", "N1.1",
"fr", "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, 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 voix pour que si le quorum avait voté.", "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é.",
), ),
( (
"N1.1", "N1.1",
@@ -675,6 +695,24 @@ const PHRASES_T3: [(&str, &str, &str, &str); 12] = [
"\n\nEjemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 72, 81, 90, 100 o 108 votos a favor, del nivel 1 al nivel 5; en el nivel 3, el 75 % de los 120 votantes, es decir 90. Por debajo del quórum, los que faltan cuentan como «no»: en el nivel 2 (quórum 12), 9 votos a favor pasan con 3 en contra como mucho; en el nivel 3 (quórum 50), hacen falta 39 votos a favor, con 11 en contra como mucho.", "\n\nEjemplo: con 7224 miembros y una participación habitual de 120 votantes, cuando votan 120 personas, hacen falta 72, 81, 90, 100 o 108 votos a favor, del nivel 1 al nivel 5; en el nivel 3, el 75 % de los 120 votantes, es decir 90. Por debajo del quórum, los que faltan cuentan como «no»: en el nivel 2 (quórum 12), 9 votos a favor pasan con 3 en contra como mucho; en el nivel 3 (quórum 50), hacen falta 39 votos a favor, con 11 en contra como mucho.",
"", "",
), ),
(
"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.",
),
(
"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.",
),
(
"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.",
),
]; ];
/// The texts of T2 with the phrases of lot T3 written in: what the seed gives today. /// The texts of T2 with the phrases of lot T3 written in: what the seed gives today.
@@ -717,7 +755,8 @@ fn participations(w: u32, vote_floor: f64) -> [f64; 4] {
} }
/// N1.1, the phrase of lot T3: « sous le quorum, les votants qui manquent comptent comme « non », /// N1.1, the phrase of lot T3: « sous le quorum, les votants qui manquent comptent comme « non »,
/// et il faut autant de voix pour que si le quorum avait voté ». True for every quorum an author /// 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 /// 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 /// 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 /// than the quorum, are the threshold of the quorum; and under it, a tally passes exactly when
@@ -727,7 +766,7 @@ fn participations(w: u32, vote_floor: f64) -> [f64; 4] {
fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() { fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
let today = today_seed(); let today = today_seed();
let said = [ let said = [
("fr", "et il faut autant de voix pour que si le quorum avait voté."), ("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."), ("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."), ("es", "y hacen falta tantos votos a favor como si hubiera votado el quórum."),
]; ];
@@ -791,6 +830,49 @@ 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)); 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)
}
}
/// 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 });
}
}
}
}
grid
}
/// 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 /// 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 /// 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 /// it does not (a web of fewer than 5 000 members and a median under 50 at a quorum of 50, an
@@ -799,31 +881,19 @@ fn under_the_quorum_it_takes_as_many_for_as_if_the_quorum_had_voted() {
#[test] #[test]
fn the_share_of_a_level_as_usual_is_asked_beyond_the_quorum() { fn the_share_of_a_level_as_usual_is_asked_beyond_the_quorum() {
let mut under = 0; let mut under = 0;
for w in ELECTORATES { for c in as_usual_grid() {
let medians = [w / 100 + 1, 12, 40, 50, 100, 120, 1000, w / 2]; let (s, m) = (c.k.share_pct() / 100.0, c.k.majority_pct() / 100.0);
for usual in medians { let params = as_usual_params(&c);
let part = Participation::of(w, 50.0, Some(f64::from(usual)), 9); let at = c.at();
if part.bounded { let asked = required_ratio(c.w, judged_voters(c.usual, c.q), &params);
continue; if c.usual >= c.q {
}
let scale = Scale { electorate: w, participation: part.ratio, quorums: [12, 12, 50, 50, 50] };
for k in InertiaLevel::ALL {
let (s, m) = (k.share_pct() / 100.0, k.majority_pct() / 100.0);
for q in [2, 12, 50, 100, 1000, usual, usual + 1, w].into_iter().filter(|q| (2..=w).contains(q)) {
let params = scale.params(k, q);
let at = format!("W {w} usual {usual} k {} Q {q}", k.get());
let asked = required_ratio(w, judged_voters(usual, q), &params);
if usual >= q {
assert!((asked - s).abs() < 1e-9, "{at}: {asked}, not s"); assert!((asked - s).abs() < 1e-9, "{at}: {asked}, not s");
} else { } else {
assert!(asked < s - 1e-9, "{at}: {asked}"); assert!(asked < s - 1e-9, "{at}: {asked}");
under += 1; under += 1;
} }
// Everyone votes: M, at every quorum. // Everyone votes: M, at every quorum.
assert!((required_ratio(w, judged_voters(w, q), &params) - m).abs() < 1e-12, "{at}: M"); assert!((required_ratio(c.w, judged_voters(c.w, c.q), &params) - m).abs() < 1e-12, "{at}: M");
}
}
}
} }
assert!(under > 0); assert!(under > 0);
// The case of F1.2, its default level and quorum: W 3 000, a usual turnout of 40 voters // The case of F1.2, its default level and quorum: W 3 000, a usual turnout of 40 voters
@@ -842,6 +912,106 @@ 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"); 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.
#[test]
fn g_gives_the_share_of_the_level_beyond_the_quorum_when_the_group_votes_as_usual() {
let today = today_seed();
// The sentence of G closes N1.1, in each language; nothing of the sentence of T3 is left.
for lang in LANGS {
let (_, body) = text(&today, "N1.1", lang);
let (_, _, before, now) = PHRASES_T3
.iter()
.find(|(c, l, _, new)| (*c, *l) == ("N1.1", lang) && new.starts_with("**G**"))
.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");
let (said_now, _) = now.split_once(" = ").expect("the formula");
assert!(said_now.contains(beyond), "N1.1 {lang}: « {beyond} »");
}
// 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.
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 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, &params) - s).abs() < 1e-9, "{at}: s on the curve");
let judged = judged_voters(c.usual, c.q);
let asked = required_ratio(c.w, judged, &params);
if c.usual >= c.q {
assert_eq!(judged, c.usual, "{at}");
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 %.
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);
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);
let needed = wot_threshold(w, usual, &p).expect("valid");
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
/// 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
/// paragraph says after it bring it back. The voted text is not changed for this.
#[test]
fn the_reference_turnout_is_a_parameter_the_bounds_bring_back() {
let seed = today_seed();
let f = figures(&seed);
let vote_floor = f64::from(f.reference) / 2.0;
for w in ELECTORATES.into_iter().chain([199, 200, 10_000, 10_001]) {
let p0 = Participation::of(w, vote_floor, None, 0);
let within = (200..=10_000).contains(&w);
assert_eq!(p0.bounded, !within, "W {w}");
if within {
assert!((p0.voters(w) - f64::from(f.reference)).abs() < 1e-9, "W {w}: {} voters", p0.voters(w));
} else {
let bound = if w < 200 { Participation::HIGHEST } else { Participation::LOWEST };
assert_eq!(p0.ratio, bound, "W {w}");
}
}
// 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"),
] {
let (_, body) = text(&seed, "N1.1", lang);
let at = |s: &str| body.find(s).unwrap_or_else(|| panic!("N1.1 {lang}: « {s} »"));
assert!(at(&reference) < at(bounds), "N1.1 {lang}: the bounds after the reference");
}
}
/// F1.1, « B : la base ; à la puissance W, elle est si petite qu'elle ne compte pas », and F1.2, /// F1.1, « B : la base ; à la puissance W, elle est si petite qu'elle ne compte pas », and F1.2,
/// « dans la toile de confiance, `B^W` est si petit qu'il ne compte pas »: true on every W of the /// « dans la toile de confiance, `B^W` est si petit qu'il ne compte pas »: true on every W of the
/// web of trust (`wot::PLAUSIBLE`, 100 to 10 000 000) — the engine's threshold is the one /// web of trust (`wot::PLAUSIBLE`, 100 to 10 000 000) — the engine's threshold is the one