@jjlmoya/utils-civic 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +6 -0
  4. package/src/index.ts +2 -0
  5. package/src/pages/[locale]/[slug].astro +7 -4
  6. package/src/tests/locale_completeness.test.ts +1 -1
  7. package/src/tests/tool_validation.test.ts +1 -1
  8. package/src/tool/civic-priority-matrix/bibliography.astro +6 -0
  9. package/src/tool/civic-priority-matrix/bibliography.ts +12 -0
  10. package/src/tool/civic-priority-matrix/civic-priority-matrix.css +531 -0
  11. package/src/tool/civic-priority-matrix/component.astro +108 -0
  12. package/src/tool/civic-priority-matrix/contract.test.ts +16 -0
  13. package/src/tool/civic-priority-matrix/controller.ts +168 -0
  14. package/src/tool/civic-priority-matrix/dom-views.ts +123 -0
  15. package/src/tool/civic-priority-matrix/entry.ts +28 -0
  16. package/src/tool/civic-priority-matrix/evaluator.ts +13 -0
  17. package/src/tool/civic-priority-matrix/i18n/de.ts +52 -0
  18. package/src/tool/civic-priority-matrix/i18n/en.ts +73 -0
  19. package/src/tool/civic-priority-matrix/i18n/es.ts +52 -0
  20. package/src/tool/civic-priority-matrix/i18n/fr.ts +50 -0
  21. package/src/tool/civic-priority-matrix/i18n/id.ts +50 -0
  22. package/src/tool/civic-priority-matrix/i18n/it.ts +50 -0
  23. package/src/tool/civic-priority-matrix/i18n/ja.ts +121 -0
  24. package/src/tool/civic-priority-matrix/i18n/ko.ts +121 -0
  25. package/src/tool/civic-priority-matrix/i18n/nl.ts +45 -0
  26. package/src/tool/civic-priority-matrix/i18n/pl.ts +35 -0
  27. package/src/tool/civic-priority-matrix/i18n/pt.ts +32 -0
  28. package/src/tool/civic-priority-matrix/i18n/ru.ts +32 -0
  29. package/src/tool/civic-priority-matrix/i18n/sv.ts +32 -0
  30. package/src/tool/civic-priority-matrix/i18n/tr.ts +32 -0
  31. package/src/tool/civic-priority-matrix/i18n/zh.ts +121 -0
  32. package/src/tool/civic-priority-matrix/index.ts +11 -0
  33. package/src/tool/civic-priority-matrix/logic.test.ts +68 -0
  34. package/src/tool/civic-priority-matrix/logic.ts +124 -0
  35. package/src/tool/civic-priority-matrix/seo.astro +12 -0
  36. package/src/tool/civic-priority-matrix/storage.ts +50 -0
  37. package/src/tool/civic-priority-matrix/ui.ts +96 -0
  38. package/src/tool/referendum-threshold-calculator/bibliography.astro +6 -0
  39. package/src/tool/referendum-threshold-calculator/bibliography.ts +6 -0
  40. package/src/tool/referendum-threshold-calculator/component.astro +117 -0
  41. package/src/tool/referendum-threshold-calculator/controller.ts +141 -0
  42. package/src/tool/referendum-threshold-calculator/dom-views.ts +71 -0
  43. package/src/tool/referendum-threshold-calculator/entry.ts +27 -0
  44. package/src/tool/referendum-threshold-calculator/evaluator.ts +19 -0
  45. package/src/tool/referendum-threshold-calculator/i18n/de.ts +56 -0
  46. package/src/tool/referendum-threshold-calculator/i18n/en.ts +79 -0
  47. package/src/tool/referendum-threshold-calculator/i18n/es.ts +56 -0
  48. package/src/tool/referendum-threshold-calculator/i18n/fr.ts +56 -0
  49. package/src/tool/referendum-threshold-calculator/i18n/id.ts +56 -0
  50. package/src/tool/referendum-threshold-calculator/i18n/it.ts +56 -0
  51. package/src/tool/referendum-threshold-calculator/i18n/ja.ts +56 -0
  52. package/src/tool/referendum-threshold-calculator/i18n/ko.ts +56 -0
  53. package/src/tool/referendum-threshold-calculator/i18n/nl.ts +56 -0
  54. package/src/tool/referendum-threshold-calculator/i18n/pl.ts +56 -0
  55. package/src/tool/referendum-threshold-calculator/i18n/pt.ts +56 -0
  56. package/src/tool/referendum-threshold-calculator/i18n/ru.ts +56 -0
  57. package/src/tool/referendum-threshold-calculator/i18n/sv.ts +56 -0
  58. package/src/tool/referendum-threshold-calculator/i18n/tr.ts +56 -0
  59. package/src/tool/referendum-threshold-calculator/i18n/zh.ts +56 -0
  60. package/src/tool/referendum-threshold-calculator/index.ts +11 -0
  61. package/src/tool/referendum-threshold-calculator/logic.test.ts +69 -0
  62. package/src/tool/referendum-threshold-calculator/logic.ts +120 -0
  63. package/src/tool/referendum-threshold-calculator/referendum-threshold-calculator.css +640 -0
  64. package/src/tool/referendum-threshold-calculator/seo.astro +14 -0
  65. package/src/tool/referendum-threshold-calculator/sharing.test.ts +23 -0
  66. package/src/tool/referendum-threshold-calculator/sharing.ts +42 -0
  67. package/src/tool/referendum-threshold-calculator/storage.ts +39 -0
  68. package/src/tool/referendum-threshold-calculator/ui.ts +126 -0
  69. package/src/tools.ts +4 -0
@@ -0,0 +1,56 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import { bibliography } from '../bibliography';
4
+ import type { ReferendumThresholdLocaleContent } from '../entry';
5
+ import type { ReferendumThresholdUI } from '../ui';
6
+
7
+ const ui: ReferendumThresholdUI = {
8
+ countsHeading: '输入公布的票数', eligibleLabel: '有投票资格或已登记的选民', eligibleHelp: '使用所检查规则中指定的选民范围。', yesLabel: '赞成票', noLabel: '反对票', blankLabel: '空白票', invalidLabel: '无效票', participationHeading: '参与门槛', participationHelp: '选择哪些票计入参与,以及必须达到的门槛。', approvalHeading: '赞成门槛', approvalHelp: '选择赞成票的分母。每条规则都会显示在结果中。', thresholdModeLabel: '门槛类型', comparisonLabel: '比较方式', atLeastOption: '至少', moreThanOption: '超过', percentageOption: '百分比', absoluteOption: '绝对数量', thresholdValueLabel: '门槛数值', denominatorLabel: '使用的计数', allBallotsOption: '所有选票', validVotesOption: '仅有效票', eligibleVotersOption: '有投票资格的选民', presetsLabel: '赞成预设', simpleMajorityPreset: '简单多数', electorateMajorityPreset: '选民多数', qualified55Preset: '55%特别多数', qualified60Preset: '60%特别多数', loadExample: '加载示例', shareAction: '复制永久链接', shareSuccess: '永久链接已复制,包含票数和两条规则。', shareFailure: '永久链接已显示在地址栏中。', resultHeading: '按照公布规则作出判断', emptyResult: '输入票数和门槛即可查看两项检验。', passed: '两项门槛都已达到', participationFailed: '未达到参与门槛', approvalFailed: '未达到赞成门槛', bothFailed: '两项门槛都未达到', invalidResult: '请检查公布的票数', participationMetric: '参与检验', approvalMetric: '赞成检验', observedLabel: '实际值', thresholdLabel: '要求值', decisionPathLabel: '判断路径', methodHeading: '采用的方法', methodText: '参与率是所选参与数量除以选民数量。赞成率是赞成票除以所选分母。比较方式决定相等是否足够,还是必须超过门槛。绝对门槛直接比较数量。', limitsHeading: '本工具不会做什么', limitsText: '它不会确定主管机关、认证计票结果、判断法定期限或提供法律建议。结果取决于输入的前提。', warningsHeading: '边界情况和数据提示', warningsText: '请确认空白票和无效票是否计入参与,选民数量是否为最新,以及赞成规则使用有效票、所有选票还是选民作为分母。', invalidInput: '请输入非负整数。选民数必须大于零,选票数不能超过选民数。', ballotsExceedEligible: '公布的选票数超过选民数。', noValidVotes: '没有赞成票或反对票,因此不存在有效票分母。', noBallots: '尚未公布选票数。', peopleSuffix: '人', yesShareLabel: '赞成票比例', allBallotsLabel: '所有选票', validVotesLabel: '有效票', eligibleVotersLabel: '选民', whatNextLabel: '使用结果前', whatNextText: '复制适用规则的准确文字,并与页面显示的配置进行比较。',
9
+ };
10
+
11
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: '公投门槛计算器', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: '显示分子和分母,检查参与和赞成门槛。', url: 'https://gamebob.dev/zh/vote-referendum-threshold-calculator', offers: { '@type': 'Offer', price: '0', priceCurrency: 'CNY' } };
12
+ const faqPage: FAQPage = { '@type': 'FAQPage', mainEntity: [
13
+ { '@type': 'Question', name: '这个计算器判断什么?', acceptedAnswer: { '@type': 'Answer', text: '它把票数与输入的参与和赞成门槛进行比较,但不会判断哪项法律规则适用。' } },
14
+ { '@type': 'Question', name: '参与和赞成有什么区别?', acceptedAnswer: { '@type': 'Answer', text: '参与检验是否有足够的人或选票达到门槛。赞成检验赞成票是否达到规定的数量或比例。' } },
15
+ { '@type': 'Question', name: '为什么空白票和无效票会改变结果?', acceptedAnswer: { '@type': 'Answer', text: '它们可能计入参与,却被排除在有效票之外。工具会分别显示每个分母。' } },
16
+ { '@type': 'Question', name: '通过结果能证明法律有效吗?', acceptedAnswer: { '@type': 'Answer', text: '不能。请查看宪法、法律、官方指引、选票定义和计票机关。' } },
17
+ ] };
18
+ const howTo: HowTo = { '@type': 'HowTo', name: '检查公投门槛', step: [
19
+ { '@type': 'HowToStep', name: '输入选民数', text: '输入规则所使用的登记选民或有投票资格的选民数。' },
20
+ { '@type': 'HowToStep', name: '分开票数', text: '输入同一公投中的赞成、反对、空白和无效票。' },
21
+ { '@type': 'HowToStep', name: '设定参与规则', text: '选择计数方式以及百分比或绝对参与门槛。' },
22
+ { '@type': 'HowToStep', name: '设定赞成规则', text: '选择分母和赞成票的要求。' },
23
+ { '@type': 'HowToStep', name: '阅读两项检验', text: '检查分子、分母、比例、门槛和数据提示。' },
24
+ ] };
25
+
26
+ export const content: ToolLocaleContent<ReferendumThresholdLocaleContent['ui']> = {
27
+ slug: 'referendum-threshold-calculator', title: '公投门槛计算器', description: '清楚显示每个分子、分母和规则,检查公投的参与及赞成门槛。', ui, seo: [
28
+ { type: 'title', text: '用可见分母检查公投规则', level: 2 },
29
+ { type: 'paragraph', html: '公投结果看似简单,但分母会改变它的含义。本计算器把选民数、选票类别以及参与和赞成两项门槛放在同一份可核查的结果中。' },
30
+ { type: 'paragraph', html: '参与和赞成是不同的检验。空白票和无效票可能增加参与数量,却不属于有效票。工具不会默默替你选择这种定义。' },
31
+ { type: 'title', text: '两项门槛如何运作', level: 2 },
32
+ { type: 'paragraph', html: '参与率用所选参与票数除以选民数。赞成率用赞成票除以所选分母。比较方式可以接受相等,也可以要求实际值超过门槛。' },
33
+ { type: 'table', headers: ['检验', '实际值', '需要回答的问题'], rows: [['参与', '所选选票 / 选民', '是否有足够比例的选民参加?'], ['赞成', '赞成 / 所选分母', '赞成票是否达到规定门槛?']] },
34
+ { type: 'title', text: '准备票数时不要混淆定义', level: 2 },
35
+ { type: 'paragraph', html: '请使用同一次投票和同一版本选民名册中的数字。选择分母之前,把赞成、反对、空白和无效票分开记录,这样每个数字都能追溯。' },
36
+ { type: 'list', items: ['确认规则指的是登记选民还是有投票资格的选民。', '分别记录四类选票。', '确认选票总数没有超过选民数。', '记录每个百分比使用的分母。', '把官方规则原文与计算结果一起保存。'] },
37
+ { type: 'tip', title: '分母是规则的一部分', html: '有效票中的50%与所有选票或全部选民中的50%不是同一项检验。空白票和无效票可能改变判断。' },
38
+ { type: 'title', text: '理解通过和失败状态', level: 2 },
39
+ { type: 'paragraph', html: '结果会区分参与失败和赞成失败。如果参与通过而赞成失败,请检查赞成票及其分母。如果相反,则参与条件尚未满足。' },
40
+ { type: 'paragraph', html: '计算通过不等于法律结论。不同法域可能有特殊条件、地区条件,或对空白票和无效票采用不同的官方处理方式。工具只评估输入的配置。' },
41
+ { type: 'tip', title: '把结果当作核对表', html: '发布结论前,把每个分子、分母、模式和比较方式与现行规则逐项核对。如果定义不清,请保留提示,不要给出过度确定的结论。' },
42
+ { type: 'title', text: '资料和限制', level: 2 },
43
+ { type: 'paragraph', html: 'International IDEA 说明参与和赞成规则为何应当预先明确。威尼斯委员会讨论门槛设计的影响。这些资料支持核查方法,但不提供适用于所有国家的统一门槛。' },
44
+ ], faq: [
45
+ { question: '这个计算器判断什么?', answer: '它把票数与输入的参与和赞成门槛进行比较,但不会判断哪项法律规则适用。' },
46
+ { question: '参与和赞成有什么区别?', answer: '参与检验是否有足够的人或选票达到门槛。赞成检验赞成票是否达到规定的数量或比例。' },
47
+ { question: '为什么空白票和无效票会改变结果?', answer: '它们可能计入参与,却被排除在有效票之外。工具会分别显示每个分母。' },
48
+ { question: '通过结果能证明法律有效吗?', answer: '不能。请查看宪法、法律、官方指引、选票定义和计票机关。' },
49
+ ], bibliography, howTo: [
50
+ { name: '输入选民数', text: '输入规则所使用的登记选民或有投票资格的选民数。' },
51
+ { name: '分开票数', text: '输入同一公投中的赞成、反对、空白和无效票。' },
52
+ { name: '设定参与规则', text: '选择计数方式以及百分比或绝对参与门槛。' },
53
+ { name: '设定赞成规则', text: '选择分母和赞成票的要求。' },
54
+ { name: '阅读两项检验', text: '检查分子、分母、比例、门槛和数据提示。' },
55
+ ], schemas: [softwareApplication, faqPage, howTo] as unknown as Record<string, unknown>[]
56
+ };
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { referendumThresholdCalculator } from './entry';
3
+
4
+ export * from './entry';
5
+ export * from './logic';
6
+ export const REFERENDUM_THRESHOLD_CALCULATOR_TOOL: ToolDefinition = {
7
+ entry: referendumThresholdCalculator,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { evaluateReferendum, getExampleInput, type ReferendumInput } from './logic';
3
+
4
+ const withChanges = (changes: Partial<ReferendumInput['counts']>): ReferendumInput => ({ ...getExampleInput(), counts: { ...getExampleInput().counts, ...changes } });
5
+
6
+ describe('referendum threshold logic', () => {
7
+ it('passes the documented example with inclusive percentage thresholds', () => {
8
+ const result = evaluateReferendum(getExampleInput());
9
+ expect(result.status).toBe('passed');
10
+ expect(result.participation).toMatchObject({ numerator: 850, denominator: 1000, rate: 85, passed: true });
11
+ expect(result.approval).toMatchObject({ numerator: 520, denominator: 820, passed: true });
12
+ });
13
+
14
+ it('keeps blank and invalid ballots in all ballots but not valid votes', () => {
15
+ const result = evaluateReferendum(withChanges({ yes: 10, no: 10, blank: 30, invalid: 50 }));
16
+ expect(result.totals).toEqual({ allBallots: 100, validVotes: 20, participationCount: 100 });
17
+ expect(result.approval.denominator).toBe(20);
18
+ });
19
+
20
+ it('distinguishes participation failure from approval failure', () => {
21
+ const participation = evaluateReferendum(withChanges({ yes: 200, no: 100, blank: 10, invalid: 0 }));
22
+ expect(participation.status).toBe('participation-failed');
23
+ const approval = evaluateReferendum({ ...withChanges({ yes: 300, no: 500, blank: 20, invalid: 10 }), rules: { ...getExampleInput().rules, approval: { mode: 'percentage', value: 50, denominator: 'valid-votes' } } });
24
+ expect(approval.status).toBe('approval-failed');
25
+ });
26
+
27
+ it('supports absolute participation and approval thresholds', () => {
28
+ const input = getExampleInput();
29
+ input.rules = { participation: { mode: 'absolute', value: 800, denominator: 'all-ballots' }, approval: { mode: 'absolute', value: 500, denominator: 'valid-votes' } };
30
+ const result = evaluateReferendum(input);
31
+ expect(result.status).toBe('passed');
32
+ });
33
+
34
+ it('supports approval thresholds over the electorate and strict majorities', () => {
35
+ const input = getExampleInput();
36
+ input.rules.approval = { mode: 'percentage', value: 50, denominator: 'eligible-voters', comparison: 'more-than' };
37
+ const result = evaluateReferendum(input);
38
+ expect(result.approval).toMatchObject({ numerator: 520, denominator: 1000, rate: 52, passed: true });
39
+ input.counts.yes = 500;
40
+ input.counts.no = 300;
41
+ expect(evaluateReferendum(input).approval.passed).toBe(false);
42
+ });
43
+
44
+ it('fails safely when approval has no denominator', () => {
45
+ const result = evaluateReferendum(withChanges({ yes: 0, no: 0, blank: 20, invalid: 5 }));
46
+ expect(result.valid).toBe(true);
47
+ expect(result.approval.rate).toBe(0);
48
+ expect(result.approval.passed).toBe(false);
49
+ expect(result.warnings).toContain('no-valid-votes');
50
+ });
51
+
52
+ it('rejects impossible totals and invalid threshold values', () => {
53
+ const impossible = evaluateReferendum(withChanges({ eligibleVoters: 10, yes: 20 }));
54
+ expect(impossible.status).toBe('invalid');
55
+ expect(impossible.errors).toContain('ballots-exceed-eligible');
56
+ const invalidThreshold = evaluateReferendum({ ...getExampleInput(), rules: { ...getExampleInput().rules, approval: { mode: 'percentage', value: 101, denominator: 'valid-votes' } } });
57
+ expect(invalidThreshold.errors).toContain('invalid-percentage');
58
+ });
59
+
60
+ it('treats zero thresholds as valid only when the required denominator exists', () => {
61
+ const input = getExampleInput();
62
+ input.rules = { participation: { mode: 'percentage', value: 0, denominator: 'all-ballots' }, approval: { mode: 'percentage', value: 0, denominator: 'valid-votes' } };
63
+ const result = evaluateReferendum(input);
64
+ expect(result.participation.passed).toBe(true);
65
+ expect(result.approval.passed).toBe(true);
66
+ const empty = evaluateReferendum({ ...input, counts: { eligibleVoters: 100, yes: 0, no: 0, blank: 0, invalid: 0 } });
67
+ expect(empty.approval.passed).toBe(false);
68
+ });
69
+ });
@@ -0,0 +1,120 @@
1
+ export type ThresholdMode = 'percentage' | 'absolute';
2
+ export type DenominatorMode = 'all-ballots' | 'valid-votes' | 'eligible-voters';
3
+ export type ThresholdComparison = 'at-least' | 'more-than';
4
+
5
+ export interface BallotCounts {
6
+ eligibleVoters: number;
7
+ yes: number;
8
+ no: number;
9
+ blank: number;
10
+ invalid: number;
11
+ }
12
+
13
+ export interface ThresholdRule {
14
+ mode: ThresholdMode;
15
+ value: number;
16
+ denominator: DenominatorMode;
17
+ comparison?: ThresholdComparison;
18
+ }
19
+
20
+ export interface RuleSet {
21
+ participation: ThresholdRule;
22
+ approval: ThresholdRule;
23
+ }
24
+
25
+ export interface ReferendumInput {
26
+ counts: BallotCounts;
27
+ rules: RuleSet;
28
+ }
29
+
30
+ export interface Totals {
31
+ allBallots: number;
32
+ validVotes: number;
33
+ participationCount: number;
34
+ }
35
+
36
+ export interface RuleEvaluation {
37
+ numerator: number;
38
+ denominator: number;
39
+ rate: number;
40
+ mode: ThresholdMode;
41
+ comparison: ThresholdComparison;
42
+ threshold: number;
43
+ passed: boolean;
44
+ denominatorMode: DenominatorMode;
45
+ }
46
+
47
+ export type ReferendumStatus = 'passed' | 'participation-failed' | 'approval-failed' | 'both-failed' | 'invalid';
48
+
49
+ export interface ReferendumEvaluation {
50
+ valid: boolean;
51
+ errors: string[];
52
+ warnings: string[];
53
+ totals: Totals;
54
+ participation: RuleEvaluation;
55
+ approval: RuleEvaluation;
56
+ status: ReferendumStatus;
57
+ }
58
+
59
+ const emptyRule: RuleEvaluation = { numerator: 0, denominator: 0, rate: 0, mode: 'percentage', comparison: 'at-least', threshold: 0, passed: false, denominatorMode: 'valid-votes' };
60
+
61
+ function isWholeNonnegative(value: number): boolean {
62
+ return Number.isFinite(value) && Number.isInteger(value) && value >= 0;
63
+ }
64
+
65
+ function validateCounts(counts: BallotCounts): string[] {
66
+ if (!Object.values(counts).every(isWholeNonnegative) || counts.eligibleVoters === 0) return ['invalid-counts'];
67
+ if (counts.yes + counts.no + counts.blank + counts.invalid > counts.eligibleVoters) return ['ballots-exceed-eligible'];
68
+ return [];
69
+ }
70
+
71
+ function validateRule(rule: ThresholdRule): string[] {
72
+ if (!Number.isFinite(rule.value) || rule.value < 0) return ['invalid-threshold'];
73
+ if (rule.mode === 'percentage' && rule.value > 100) return ['invalid-percentage'];
74
+ if (rule.mode === 'absolute' && !Number.isInteger(rule.value)) return ['invalid-absolute-threshold'];
75
+ return [];
76
+ }
77
+
78
+ function getDenominator(counts: BallotCounts, mode: DenominatorMode): number {
79
+ if (mode === 'all-ballots') return counts.yes + counts.no + counts.blank + counts.invalid;
80
+ if (mode === 'valid-votes') return counts.yes + counts.no;
81
+ return counts.eligibleVoters;
82
+ }
83
+
84
+ function meetsThreshold(observed: number, threshold: number, comparison: ThresholdComparison): boolean {
85
+ return comparison === 'more-than' ? observed > threshold : observed >= threshold;
86
+ }
87
+
88
+ function evaluateRule(rule: ThresholdRule, numerator: number, denominator: number): RuleEvaluation {
89
+ const comparison = rule.comparison ?? 'at-least';
90
+ const rate = denominator > 0 ? (numerator / denominator) * 100 : 0;
91
+ const passed = rule.mode === 'percentage' ? denominator > 0 && meetsThreshold(rate, rule.value, comparison) : meetsThreshold(numerator, rule.value, comparison);
92
+ return { numerator, denominator, rate, mode: rule.mode, comparison, threshold: rule.value, passed, denominatorMode: rule.denominator };
93
+ }
94
+
95
+ function getStatus(participation: RuleEvaluation, approval: RuleEvaluation): ReferendumStatus {
96
+ if (participation.passed && approval.passed) return 'passed';
97
+ if (!participation.passed && !approval.passed) return 'both-failed';
98
+ return participation.passed ? 'approval-failed' : 'participation-failed';
99
+ }
100
+
101
+ export function evaluateReferendum(input: ReferendumInput): ReferendumEvaluation {
102
+ const errors = [...new Set([...validateCounts(input.counts), ...validateRule(input.rules.participation), ...validateRule(input.rules.approval)])];
103
+ const allBallots = input.counts.yes + input.counts.no + input.counts.blank + input.counts.invalid;
104
+ const validVotes = input.counts.yes + input.counts.no;
105
+ const participationCount = getDenominator(input.counts, input.rules.participation.denominator);
106
+ const totals: Totals = { allBallots, validVotes, participationCount };
107
+ const participation = evaluateRule(input.rules.participation, participationCount, input.counts.eligibleVoters);
108
+ const approval = evaluateRule(input.rules.approval, input.counts.yes, getDenominator(input.counts, input.rules.approval.denominator));
109
+ const warnings: string[] = [];
110
+ if (allBallots === 0) warnings.push('no-ballots');
111
+ if (validVotes === 0) warnings.push('no-valid-votes');
112
+ if (errors.length > 0) return { valid: false, errors, warnings, totals, participation, approval, status: 'invalid' };
113
+ return { valid: true, errors: [], warnings, totals, participation, approval, status: getStatus(participation, approval) };
114
+ }
115
+
116
+ export function getExampleInput(): ReferendumInput {
117
+ return { counts: { eligibleVoters: 1000, yes: 520, no: 300, blank: 20, invalid: 10 }, rules: { participation: { mode: 'percentage', value: 50, denominator: 'all-ballots', comparison: 'at-least' }, approval: { mode: 'percentage', value: 50, denominator: 'valid-votes', comparison: 'at-least' } } };
118
+ }
119
+
120
+ export const EMPTY_RULE_EVALUATION = emptyRule;