UPG-100 batch selection: a state-only selection prints its Prayer list and Card deck
The packet and card-deck title named the selection's zones and countries only. Since India's states bring people, a selection of states alone had no names, and reduce() threw "Reduce of empty array with no initial value" at the reader. selectionTitleNames (data/selection-export.js, pure) adds the states and union territories that brought people. A province with no table is not named. A selection that brought nobody (Mizoram or Nagaland alone) gives the keyed "no people groups to print" instead. check:batch-selection-export arm A6 tests it on the real served table: Gujarāt alone is titled "Gujarāt", a state + a country names both, a province beside it is not named, and Mizoram alone reads the keyed text. Its negative control shows the old naming is empty and reduce() throws. The call site is pinned in arm C. Four mutants each fail.
This commit is contained in:
parent
8631748258
commit
31e5c3e5f9
3 changed files with 56 additions and 4 deletions
|
|
@ -112,6 +112,38 @@ ok(!noCountry.length && resolveTerritory({ shapeName: 'Mahārāshtra' }).country
|
|||
ok(!disagree.length, `Copy's regroup warning disagrees with apply-grouping: ${disagree.join('; ')}`,
|
||||
`the regroup warning matches apply-grouping's own verdict in all ${Object.keys(REGROUP_CASES).length} cases (India, China, countries, and the three it refuses)`)
|
||||
|
||||
// ── A6. A STATE-ONLY selection titles its Prayer list / Card deck ───────────────────────────────────────
|
||||
// Development threw "Reduce of empty array" for a state-only selection: the title named zones and countries only, so
|
||||
// a selection of Gujarāt alone had NO names. On the REAL served table: Gujarāt alone is titled "Gujarāt"; a state +
|
||||
// a country names both; a province with no table beside it is not named; Mizoram (no groups listed) alone gives the
|
||||
// keyed "no people groups" text. NEGATIVE CONTROL: the old naming (zones + countries) gives [] and reduce() throws.
|
||||
{
|
||||
const { keyedError, errorText } = await import(pathToFileURL(P('library/i18n/keyedError.js')).href)
|
||||
const wallEn = JSON.parse(readFileSync(P('library/i18n/locales/en/wall.json'), 'utf8'))
|
||||
const tr = (k) => k.split('.').slice(1).reduce((o, x) => (o == null ? o : o[x]), wallEn) ?? k
|
||||
const states = JSON.parse(readFileSync(P('data/people-groups/india-states.json'), 'utf8')).states
|
||||
const st = (shape) => states.find((x) => x.shapeName === shape)
|
||||
const guj = st('Gujarāt'), miz = st('Mizoram')
|
||||
const fromOf = (pairs) => { const m = new Map(); for (const [slugs, name] of pairs) for (const s of slugs) m.set(s, [...(m.get(s) || []), name]); return m }
|
||||
const noRecords = () => keyedError('wall.hud.noRecords', 'that list has no people groups to print')
|
||||
const join = (a, b) => `${a} + ${b}`
|
||||
const T = (kind, name) => ({ kind, name })
|
||||
const alone = X.selectionTitleNames([], [T('adm1', 'Gujarāt')], fromOf([[guj.slugs, 'Gujarāt']]), noRecords)
|
||||
const both = X.selectionTitleNames([], [T('adm0', 'Nepal'), T('adm1', 'Gujarāt')], fromOf([[['npl-a', 'npl-b'], 'Nepal'], [guj.slugs, 'Gujarāt']]), noRecords)
|
||||
const withProvince = X.selectionTitleNames([], [T('adm1', 'Guizhou Province'), T('adm1', 'Gujarāt')], fromOf([[guj.slugs, 'Gujarāt']]), noRecords)
|
||||
let empty = ''
|
||||
try { X.selectionTitleNames([], [T('adm1', 'Mizoram')], fromOf([[miz.slugs, 'Mizoram']]), noRecords); empty = 'no throw' } catch (e) { empty = errorText(e, tr) }
|
||||
let oldThrows = false
|
||||
try { [].reduce(join) } catch (_) { oldThrows = true }
|
||||
const oldNames = [...[], ...[T('adm1', 'Gujarāt')].filter((t) => t.kind === 'adm0').map((t) => t.name)]
|
||||
ok(guj.count === 90 && miz.count === 0 && oldNames.length === 0 && oldThrows,
|
||||
`NEGATIVE CONTROL: Gujarāt ${guj.count} / Mizoram ${miz.count} groups, the old naming gave ${JSON.stringify(oldNames)}`,
|
||||
'negative control: on the old naming a Gujarāt-only selection has no names, and reduce() throws')
|
||||
ok(alone.length === 1 && alone.reduce(join) === 'Gujarāt' && both.join('|') === 'Nepal|Gujarāt' && withProvince.join('|') === 'Gujarāt' && empty === tr('wall.hud.noRecords'),
|
||||
`titles: alone ${JSON.stringify(alone)}, with a country ${JSON.stringify(both)}, with a province ${JSON.stringify(withProvince)}, Mizoram alone "${empty}"`,
|
||||
`a state-only selection is titled "Gujarāt"; a state + a country names both; a province with no table is not named; Mizoram alone reads "${empty}"`)
|
||||
}
|
||||
|
||||
// ── B. the CSV ───────────────────────────────────────────────────────────────────────────────────
|
||||
function parseCsv(s) { // RFC 4180: quoted fields, doubled quotes, CRLF records
|
||||
const rows = []; let row = [], cell = '', q = false
|
||||
|
|
@ -161,6 +193,9 @@ const wired = {
|
|||
return !/toggleTerritory|toggleByProps|toggleZoneSelection/.test(body)
|
||||
})(),
|
||||
'the regroup warning on Copy': /const warn = regroupable\(sel\) \? '' : t\('upgMap\.copyNotRegroupable'\)/.test(vue),
|
||||
// The packet / card-deck title goes through selectionTitleNames (arm A6 proves it), with the keyed empty case.
|
||||
'the title names come from selectionTitleNames': /const names = selectionTitleNames\(zoneNames, selectedTerritories\.value, from,\s*\n\s*\(\) => keyedError\('wall\.hud\.noRecords'/.test(vue)
|
||||
&& !/const names = \[\.\.\.zoneNames, \.\.\.countryNames\]/.test(vue),
|
||||
'India states detected from the served table': /import \{ loadIndiaStates \} from '@map\/data\/indiaStates\.js'/.test(vue)
|
||||
&& /india\.byShapeName\.get\(t\.name\)/.test(vue) && /t\.kind === 'adm1'\) indiaStates\(\)\.catch/.test(vue),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,22 @@ export function selectionClipboard(selection, opts = {}, csv = '') {
|
|||
return csv ? head + '\n\n' + csv : head;
|
||||
}
|
||||
|
||||
/**
|
||||
* The names a selection's Prayer list and Card deck are titled by: its zones (already localized by the caller), its
|
||||
* countries, and the India states and union territories that BROUGHT people (`from`: slug → the territory names
|
||||
* that contain it). A province with no table, or a state with none listed, is not named as though it were printed.
|
||||
* ★ THE STATES WERE MISSING, and a state-only selection then had NO names: reduce() threw "Reduce of empty array"
|
||||
* at the reader. A selection that brought nobody throws `noRecords()` (the caller's keyed error) instead, so the
|
||||
* list is never empty.
|
||||
*/
|
||||
export function selectionTitleNames(zoneNames, territories, from, noRecords) {
|
||||
if (!from || !from.size) throw noRecords();
|
||||
const contributing = new Set([...from.values()].flat());
|
||||
const countries = territories.filter((t) => t.kind === 'adm0').map((t) => t.name);
|
||||
const states = territories.filter((t) => t.kind === 'adm1' && contributing.has(t.name)).map((t) => t.name);
|
||||
return [...zoneNames, ...countries, ...states];
|
||||
}
|
||||
|
||||
/** A file name a partner can recognise: the selection's label, reduced to safe characters. */
|
||||
export function selectionCsvFileName(label) {
|
||||
const base = String(label || 'selection').normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
||||
|
|
@ -81,4 +97,4 @@ export function selectionCsvFileName(label) {
|
|||
return `upg-100-${base}.csv`;
|
||||
}
|
||||
|
||||
export default { SELECTION_CSV_COLUMNS, csvCell, payloadName, regroupKind, regroupable, selectionCsv, selectionClipboard, selectionCsvFileName };
|
||||
export default { SELECTION_CSV_COLUMNS, csvCell, payloadName, regroupKind, regroupable, selectionCsv, selectionClipboard, selectionTitleNames, selectionCsvFileName };
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ import { colorForZone } from './data/zone-colors.js'
|
|||
// clicked tapestry feature carries shapeName (ADM1) OR iso3 (ADM0) and this maps
|
||||
// either to { key, name, count } from admin-counts/membership (NEVER edits them).
|
||||
import { resolveTerritory, buildCountryNameMap, formatClipboard, ADM1_COUNTS, ISO3_NAME } from './data/territory-counts.js'
|
||||
import { selectionCsv, selectionClipboard, selectionCsvFileName, regroupable } from './data/selection-export.js'
|
||||
import { selectionCsv, selectionClipboard, selectionCsvFileName, regroupable, selectionTitleNames } from './data/selection-export.js'
|
||||
// 09b-search-bar §1(a-c): unified search index (PG + zone + territory) + the
|
||||
// tokenized contains matcher. Pure data + pure fns; this profile builds the rows
|
||||
// from the live records (PG), the legend leafZones (zone), and admin-counts +
|
||||
|
|
@ -1894,8 +1894,9 @@ async function selectionAsList() {
|
|||
const lists = selectedLists();
|
||||
const { from } = await selectionPeople();
|
||||
const zoneNames = (await Promise.all(lists.map(localizedZone))).map((l) => l.label || l.id);
|
||||
const countryNames = selectedTerritories.value.filter((t) => t.kind === 'adm0').map((t) => t.name);
|
||||
const names = [...zoneNames, ...countryNames];
|
||||
// Zones, countries AND the states that brought people (data/selection-export.js says why the states matter).
|
||||
const names = selectionTitleNames(zoneNames, selectedTerritories.value, from,
|
||||
() => keyedError('wall.hud.noRecords', 'that list has no people groups to print'));
|
||||
const S = await packetStrings();
|
||||
const join2 = (a, b) => fillJoin(S.zone_join, { a, b }) ?? `${a} + ${b}`;
|
||||
const label = names.length <= 3
|
||||
|
|
|
|||
Loading…
Reference in a new issue