{"version":3,"sources":["../../../src/election/attribution/scoreFactory.ts","../../../src/election/ballots.ts"],"sourcesContent":["import { enumerate, max } from \"@gouvernathor/python\";\nimport { Counter, DefaultMap } from \"@gouvernathor/python/collections\";\nimport { fmean, median } from \"@gouvernathor/python/statistics\";\nimport { Scores } from \"../ballots\";\nimport { type Attribution, type HasNSeats } from \"../attribution\";\n\n/**\n * Creates an attribution method in which all the seats go to the candidate with the highest average score.\n *\n * The ballots are not required to grade all the candidates.\n */\nexport function averageScore<Party>(\n    { nSeats }: {\n        nSeats: number\n    }\n): Attribution<Party, Scores<Party>> & HasNSeats {\n    const attrib = (votes: Scores<Party>, rest = {}): Counter<Party> => {\n        const counts = new DefaultMap<Party, number[]>(() => []);\n        for (const [party, grades] of votes) {\n            for (const [grade, qty] of enumerate(grades)) {\n                counts.get(party).push(...Array(qty).fill(grade));\n            }\n        }\n\n        return new Counter([[max(counts.keys(), party => fmean(counts.get(party)!)), nSeats]]);\n    }\n    attrib.nSeats = nSeats;\n    return attrib;\n}\n\n/**\n * Creates an attribution method in which all the seats go to the candidate with the highest median score.\n *\n * If there is a tie, the contingency method is called on the candidates that are tied.\n * The default contingency is to take the maximum average score.\n *\n * The ballots are not required to grade all the candidates.\n */\nexport function medianScore<Party>(\n    { nSeats, contingency }: {\n        nSeats: number,\n        contingency?: Attribution<Party, Scores<Party>>,\n    }\n): Attribution<Party, Scores<Party>> & HasNSeats {\n    if (contingency === undefined) {\n        contingency = averageScore({ nSeats });\n    }\n\n    const attrib = (votes: Scores<Party>, rest = {}): Counter<Party> => {\n        const counts = new DefaultMap<Party, number[]>(() => []);\n        for (const [party, grades] of votes) {\n            for (const [grade, qty] of enumerate(grades)) {\n                counts.get(party).push(...Array(qty).fill(grade));\n            }\n        }\n\n        const medians = new Map([...counts.entries()]\n            .map(([party, partigrades]) => [party, median(partigrades)]));\n\n        const winScore = Math.max(...medians.values());\n        const [winner, ...winners] = [...medians.keys()].filter(p => medians.get(p) === winScore);\n\n        if (winners.length === 0) { // no tie\n            return new Counter([[winner, nSeats]]);\n        }\n\n        winners.unshift(winner);\n        const trimmedResults = Scores.fromEntries(winners.map(party => [party, counts.get(party)!]));\n        return contingency(trimmedResults, rest);\n    };\n    attrib.nSeats = nSeats;\n    return attrib;\n}\n","import { ReadonlyCounter } from \"@gouvernathor/python/collections\";\n\n/**\n * A counter, mapping each party to its number of ballots.\n *\n * [[PS, 5], [LR: 7]] -> 5 ballots for PS, 7 for LR.\n */\nexport interface Simple<Party> extends ReadonlyCounter<Party> { }\n\n/**\n * A list of ballots, each ballot ordering parties by decreasing preference.\n *\n * [[LR, PS, LFI], [LFI, PS,], ] -> one voter prefers LR then PS then LFI,\n * another prefers LFI then PS and didn't rank LR.\n *\n * Math.max(result.map(ballot => ballot.length)) <= number of candidates-1\n *                                               == if the voting method requires a full ranking\n *\n * There can be no tie between candidates within a ballot.\n * Note that not ranking all the candidates is permitted by this type,\n * although some attribution methods may not support it.\n */\nexport interface Order<Party> extends ReadonlyArray<ReadonlyArray<Party>> { }\n\n/**\n * A mapping from each party to a list of number of ballots, one for each grade.\n *\n * [[PS, [0, 2, 5, 9, 1]], ] -> PS received the worst grade 0 times, the best grade 1 time and so on.\n *\n * result.get(p).length is constant, equal to the number of grades of the voting method.\n *\n * If the voter must grade all the candidates, then sum(result.get(p)) is constant\n * and equal to the number of voters.\n *\n * Any party not mapped will be assumed to have\n * an array of zeros (of length ngrades).\n */\nexport interface Scores<Party> extends ReadonlyMap<Party, ReadonlyArray<number>> {\n    readonly ngrades: number;\n    get(key: Party): ReadonlyArray<number>;\n}\n\nexport namespace Scores {\n    function get<Party>(this: Scores<Party>, key: Party): ReadonlyArray<number> {\n        const value = this.get(key);\n        if (value === undefined) {\n            return Array(this.ngrades).fill(0);\n        }\n        return value!;\n    }\n\n    export function fromEntries<Party>(\n        elements: readonly [Party, readonly number[]][],\n    ): Scores<Party> {\n        if (elements.length === 0) {\n            throw new Error(\"Use the fromGrades method to create an empty Scores instance\");\n        }\n        const ths = new Map(elements) as Partial<Scores<Party>> & { ngrades?: number };\n        ths.ngrades = elements[0][1].length;\n        ths.get = get.bind(ths as any);\n        return ths as Scores<Party>;\n    }\n\n    export function fromGrades<Party>(ngrades: number): Scores<Party> {\n        const ths = new Map() as Partial<Scores<Party>> & { ngrades?: number };\n        ths.ngrades = ngrades;\n        ths.get = get.bind(ths as any);\n        return ths as Scores<Party>;\n    }\n}\n\n\nexport type Ballots<Party> = Simple<Party> | Order<Party> | Scores<Party>;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA+B;AAC/B,yBAAoC;AACpC,wBAA8B;;;ACwCvB,IAAU;AAAA,CAAV,CAAUA,YAAV;AACH,WAAS,IAAgC,KAAmC;AACxE,UAAM,QAAQ,KAAK,IAAI,GAAG;AAC1B,QAAI,UAAU,QAAW;AACrB,aAAO,MAAM,KAAK,OAAO,EAAE,KAAK,CAAC;AAAA,IACrC;AACA,WAAO;AAAA,EACX;AAEO,WAAS,YACZ,UACa;AACb,QAAI,SAAS,WAAW,GAAG;AACvB,YAAM,IAAI,MAAM,8DAA8D;AAAA,IAClF;AACA,UAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,QAAI,UAAU,SAAS,CAAC,EAAE,CAAC,EAAE;AAC7B,QAAI,MAAM,IAAI,KAAK,GAAU;AAC7B,WAAO;AAAA,EACX;AAVO,EAAAA,QAAS;AAYT,WAAS,WAAkB,SAAgC;AAC9D,UAAM,MAAM,oBAAI,IAAI;AACpB,QAAI,UAAU;AACd,QAAI,MAAM,IAAI,KAAK,GAAU;AAC7B,WAAO;AAAA,EACX;AALO,EAAAA,QAAS;AAAA,GArBH;;;AD/BV,SAAS,aACZ,EAAE,OAAO,GAGoC;AAC7C,QAAM,SAAS,CAAC,OAAsB,OAAO,CAAC,MAAsB;AAChE,UAAM,SAAS,IAAI,8BAA4B,MAAM,CAAC,CAAC;AACvD,eAAW,CAAC,OAAO,MAAM,KAAK,OAAO;AACjC,iBAAW,CAAC,OAAO,GAAG,SAAK,yBAAU,MAAM,GAAG;AAC1C,eAAO,IAAI,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,EAAE,KAAK,KAAK,CAAC;AAAA,MACpD;AAAA,IACJ;AAEA,WAAO,IAAI,2BAAQ,CAAC,KAAC,mBAAI,OAAO,KAAK,GAAG,eAAS,yBAAM,OAAO,IAAI,KAAK,CAAE,CAAC,GAAG,MAAM,CAAC,CAAC;AAAA,EACzF;AACA,SAAO,SAAS;AAChB,SAAO;AACX;AAUO,SAAS,YACZ,EAAE,QAAQ,YAAY,GAIuB;AAC7C,MAAI,gBAAgB,QAAW;AAC3B,kBAAc,aAAa,EAAE,OAAO,CAAC;AAAA,EACzC;AAEA,QAAM,SAAS,CAAC,OAAsB,OAAO,CAAC,MAAsB;AAChE,UAAM,SAAS,IAAI,8BAA4B,MAAM,CAAC,CAAC;AACvD,eAAW,CAAC,OAAO,MAAM,KAAK,OAAO;AACjC,iBAAW,CAAC,OAAO,GAAG,SAAK,yBAAU,MAAM,GAAG;AAC1C,eAAO,IAAI,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,EAAE,KAAK,KAAK,CAAC;AAAA,MACpD;AAAA,IACJ;AAEA,UAAM,UAAU,IAAI,IAAI,CAAC,GAAG,OAAO,QAAQ,CAAC,EACvC,IAAI,CAAC,CAAC,OAAO,WAAW,MAAM,CAAC,WAAO,0BAAO,WAAW,CAAC,CAAC,CAAC;AAEhE,UAAM,WAAW,KAAK,IAAI,GAAG,QAAQ,OAAO,CAAC;AAC7C,UAAM,CAAC,QAAQ,GAAG,OAAO,IAAI,CAAC,GAAG,QAAQ,KAAK,CAAC,EAAE,OAAO,OAAK,QAAQ,IAAI,CAAC,MAAM,QAAQ;AAExF,QAAI,QAAQ,WAAW,GAAG;AACtB,aAAO,IAAI,2BAAQ,CAAC,CAAC,QAAQ,MAAM,CAAC,CAAC;AAAA,IACzC;AAEA,YAAQ,QAAQ,MAAM;AACtB,UAAM,iBAAiB,OAAO,YAAY,QAAQ,IAAI,WAAS,CAAC,OAAO,OAAO,IAAI,KAAK,CAAE,CAAC,CAAC;AAC3F,WAAO,YAAY,gBAAgB,IAAI;AAAA,EAC3C;AACA,SAAO,SAAS;AAChB,SAAO;AACX;","names":["Scores"]}