@washy-washy/core 1.1.0 → 1.3.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.
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![npm](https://img.shields.io/npm/v/%40washy-washy%2Fcore)](https://www.npmjs.com/package/@washy-washy/core)
5
5
  [![Codecov](https://codecov.io/gh/alrayyes/washy-washy-core/graph/badge.svg)](https://codecov.io/gh/alrayyes/washy-washy-core)
6
6
  [![license](https://img.shields.io/github/license/alrayyes/washy-washy-core)](LICENSE)
7
+ [![docs](https://img.shields.io/badge/docs-typedoc-blue)](https://alrayyes.github.io/washy-washy-core/)
7
8
 
8
9
  Chart parsing, machine validation, and mixing rules for
9
10
  [washy-washy](https://github.com/alrayyes/washy-washy-cli), a laundry chart
@@ -61,6 +62,10 @@ const instructions = chartFromJson(jsonFromStorage, machine);
61
62
 
62
63
  ### What's in it
63
64
 
65
+ - Every chart row carries an optional `referenceName`/`referenceLink` pair —
66
+ who to credit for a care instruction that isn't obvious from the garment
67
+ itself ("the manufacturer says wash these alone"), and a link backing it
68
+ up. Both are empty strings when there's nothing to cite.
64
69
  - **`parseMachine`** validates a machine description (washer programmes,
65
70
  temperatures, spins, options; iron thermostat positions) and hands back a
66
71
  typed `Machine`. Every failure names the field that's wrong.
@@ -70,14 +75,23 @@ const instructions = chartFromJson(jsonFromStorage, machine);
70
75
  - **`parseConfig`**/**`configFromJson`**/**`configToJson`** do the same for a
71
76
  machine and a chart together, as one `{ machine, chart }` object — the
72
77
  chart is always validated against the machine it's embedded with.
78
+ `configToJson`'s output leads with a `$schema` key
79
+ ([`schema/config.schema.json`](schema/config.schema.json), generated from
80
+ the `Config` type, so it can't drift) — an editor that reads it, VS Code
81
+ among them, validates and autocompletes the file with no setup. The output
82
+ is plain `JSON.stringify(..., null, 2)` — if you write it straight to a
83
+ file in a Biome- or Prettier-linted repo, run your own formatter over it
84
+ afterward, since a generic serializer can't match either tool's opinions.
73
85
  - **`mixBlocker`**/**`canMix`**/**`resolve`** decide whether two piles of
74
86
  laundry can share a drum, and why not when they can't.
75
87
  - **`cardGroups`**/**`washGroups`**/**`ironGroups`** group instructions the
76
88
  way a printed chart is laid out: one card per identical setup, a wash-only
77
89
  cut, an iron-only cut ordered by thermostat position.
78
90
 
79
- Every exported function and type carries a TSDoc comment the full API
80
- reference is your editor's hover, not a separate generated site.
91
+ Every exported function and type carries a TSDoc comment with a runnable
92
+ example that's your editor's hover, and it's also the generated
93
+ [API reference](https://alrayyes.github.io/washy-washy-core/).
94
+ [`@washy-washy/pdf`](https://alrayyes.github.io/washy-washy-pdf/) has the same.
81
95
 
82
96
  ## Development
83
97
 
@@ -1,2 +1,2 @@
1
- import { A as variants, C as Instruction, D as colourGroups, E as Variant, F as formatTemperature, I as ironSetting, L as ironSettingKeys, M as IronSetting, N as Machine, O as durationsOf, P as Washer, R as parseMachine, S as ColourGroup, T as ResolvedInstruction, _ as washGroups, a as chartToJson, b as configToJson, c as Blocker, d as canMix, f as cardGroups, g as resolve, h as mixBlocker, i as chartFromJson, j as Iron, k as mixTags, l as blockerCode, m as loadGroups, n as Row, o as instructionsFromRows, p as ironGroups, r as RowError, s as rowsFromInstructions, t as COLUMNS, u as blockerLegend, v as Config, w as MixTag, x as parseConfig, y as configFromJson } from "./rows-C-0YQE-w.mjs";
2
- export { Blocker, COLUMNS, ColourGroup, Config, Instruction, Iron, IronSetting, Machine, MixTag, ResolvedInstruction, Row, RowError, Variant, Washer, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, configFromJson, configToJson, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseConfig, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
1
+ import { A as mixTags, C as ColourGroup, D as Variant, E as ResolvedInstruction, F as Washer, I as formatTemperature, L as ironSetting, M as Iron, N as IronSetting, O as colourGroups, P as Machine, R as ironSettingKeys, S as parseConfig, T as MixTag, _ as washGroups, a as chartToJson, b as configFromJson, c as Blocker, d as canMix, f as cardGroups, g as resolve, h as mixBlocker, i as chartFromJson, j as variants, k as durationsOf, l as blockerCode, m as loadGroups, n as Row, o as instructionsFromRows, p as ironGroups, r as RowError, s as rowsFromInstructions, t as COLUMNS, u as blockerLegend, v as CONFIG_SCHEMA_URL, w as Instruction, x as configToJson, y as Config, z as parseMachine } from "./rows-Bn1LlIaD.mjs";
2
+ export { Blocker, COLUMNS, CONFIG_SCHEMA_URL, ColourGroup, Config, Instruction, Iron, IronSetting, Machine, MixTag, ResolvedInstruction, Row, RowError, Variant, Washer, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, configFromJson, configToJson, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseConfig, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
package/dist/browser.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { C as formatTemperature, E as parseMachine, S as variants, T as ironSettingKeys, _ as instructionsFromRows, a as ironGroups, b as durationsOf, c as resolve, d as configToJson, f as parseConfig, g as chartToJson, h as chartFromJson, i as cardGroups, l as washGroups, m as RowError, n as blockerLegend, o as loadGroups, p as COLUMNS, r as canMix, s as mixBlocker, t as blockerCode, u as configFromJson, v as rowsFromInstructions, w as ironSetting, x as mixTags, y as colourGroups } from "./mixing-DzbYAriI.mjs";
2
- export { COLUMNS, RowError, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, configFromJson, configToJson, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseConfig, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
1
+ import { C as variants, D as parseMachine, E as ironSettingKeys, S as mixTags, T as ironSetting, _ as chartToJson, a as ironGroups, b as colourGroups, c as resolve, d as configFromJson, f as configToJson, g as chartFromJson, h as RowError, i as cardGroups, l as washGroups, m as COLUMNS, n as blockerLegend, o as loadGroups, p as parseConfig, r as canMix, s as mixBlocker, t as blockerCode, u as CONFIG_SCHEMA_URL, v as instructionsFromRows, w as formatTemperature, x as durationsOf, y as rowsFromInstructions } from "./mixing-Dt043zmh.mjs";
2
+ export { COLUMNS, CONFIG_SCHEMA_URL, RowError, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, configFromJson, configToJson, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseConfig, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,14 @@
1
- import { A as variants, C as Instruction, D as colourGroups, E as Variant, F as formatTemperature, I as ironSetting, L as ironSettingKeys, M as IronSetting, N as Machine, O as durationsOf, P as Washer, R as parseMachine, S as ColourGroup, T as ResolvedInstruction, _ as washGroups, a as chartToJson, b as configToJson, c as Blocker, d as canMix, f as cardGroups, g as resolve, h as mixBlocker, i as chartFromJson, j as Iron, k as mixTags, l as blockerCode, m as loadGroups, n as Row, o as instructionsFromRows, p as ironGroups, r as RowError, s as rowsFromInstructions, t as COLUMNS, u as blockerLegend, v as Config, w as MixTag, x as parseConfig, y as configFromJson } from "./rows-C-0YQE-w.mjs";
1
+ import { A as mixTags, C as ColourGroup, D as Variant, E as ResolvedInstruction, F as Washer, I as formatTemperature, L as ironSetting, M as Iron, N as IronSetting, O as colourGroups, P as Machine, R as ironSettingKeys, S as parseConfig, T as MixTag, _ as washGroups, a as chartToJson, b as configFromJson, c as Blocker, d as canMix, f as cardGroups, g as resolve, h as mixBlocker, i as chartFromJson, j as variants, k as durationsOf, l as blockerCode, m as loadGroups, n as Row, o as instructionsFromRows, p as ironGroups, r as RowError, s as rowsFromInstructions, t as COLUMNS, u as blockerLegend, v as CONFIG_SCHEMA_URL, w as Instruction, x as configToJson, y as Config, z as parseMachine } from "./rows-Bn1LlIaD.mjs";
2
2
  //#region src/csv.d.ts
3
- /** Parses the instruction CSV. See `instructionsFromRows` for the row rules. */
3
+ /**
4
+ * Parses the instruction CSV. See `instructionsFromRows` for the row rules.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const instructions = parseInstructions(csvFileContents, machine);
9
+ * ```
10
+ */
4
11
  declare function parseInstructions(source: string, machine: Machine): Instruction[];
5
12
  //#endregion
6
- export { Blocker, COLUMNS, ColourGroup, Config, Instruction, Iron, IronSetting, Machine, MixTag, ResolvedInstruction, Row, RowError, Variant, Washer, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, configFromJson, configToJson, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseConfig, parseInstructions, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
13
+ export { Blocker, COLUMNS, CONFIG_SCHEMA_URL, ColourGroup, Config, Instruction, Iron, IronSetting, Machine, MixTag, ResolvedInstruction, Row, RowError, Variant, Washer, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, configFromJson, configToJson, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseConfig, parseInstructions, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
7
14
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,7 +1,14 @@
1
- import { C as formatTemperature, E as parseMachine, S as variants, T as ironSettingKeys, _ as instructionsFromRows, a as ironGroups, b as durationsOf, c as resolve, d as configToJson, f as parseConfig, g as chartToJson, h as chartFromJson, i as cardGroups, l as washGroups, m as RowError, n as blockerLegend, o as loadGroups, p as COLUMNS, r as canMix, s as mixBlocker, t as blockerCode, u as configFromJson, v as rowsFromInstructions, w as ironSetting, x as mixTags, y as colourGroups } from "./mixing-DzbYAriI.mjs";
1
+ import { C as variants, D as parseMachine, E as ironSettingKeys, S as mixTags, T as ironSetting, _ as chartToJson, a as ironGroups, b as colourGroups, c as resolve, d as configFromJson, f as configToJson, g as chartFromJson, h as RowError, i as cardGroups, l as washGroups, m as COLUMNS, n as blockerLegend, o as loadGroups, p as parseConfig, r as canMix, s as mixBlocker, t as blockerCode, u as CONFIG_SCHEMA_URL, v as instructionsFromRows, w as formatTemperature, x as durationsOf, y as rowsFromInstructions } from "./mixing-Dt043zmh.mjs";
2
2
  import { parse } from "csv-parse/sync";
3
3
  //#region src/csv.ts
4
- /** Parses the instruction CSV. See `instructionsFromRows` for the row rules. */
4
+ /**
5
+ * Parses the instruction CSV. See `instructionsFromRows` for the row rules.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const instructions = parseInstructions(csvFileContents, machine);
10
+ * ```
11
+ */
5
12
  function parseInstructions(source, machine) {
6
13
  const records = parse(source, {
7
14
  columns: true,
@@ -13,6 +20,6 @@ function parseInstructions(source, machine) {
13
20
  return instructionsFromRows(records, machine);
14
21
  }
15
22
  //#endregion
16
- export { COLUMNS, RowError, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, configFromJson, configToJson, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseConfig, parseInstructions, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
23
+ export { COLUMNS, CONFIG_SCHEMA_URL, RowError, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, configFromJson, configToJson, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseConfig, parseInstructions, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
17
24
 
18
25
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/csv.ts"],"sourcesContent":["import { parse } from \"csv-parse/sync\";\nimport type { Machine } from \"./machine\";\nimport { instructionsFromRows } from \"./rows\";\nimport type { Instruction } from \"./types\";\n\n/** Parses the instruction CSV. See `instructionsFromRows` for the row rules. */\nexport function parseInstructions(source: string, machine: Machine): Instruction[] {\n const records: Record<string, string>[] = parse(source, {\n columns: true,\n skip_empty_lines: true,\n trim: true,\n bom: true,\n });\n if (records.length === 0) throw new Error(\"the CSV has a header but no rows\");\n return instructionsFromRows(records, machine);\n}\n"],"mappings":";;;;AAMA,SAAgB,kBAAkB,QAAgB,SAAiC;CACjF,MAAM,UAAoC,MAAM,QAAQ;EACtD,SAAS;EACT,kBAAkB;EAClB,MAAM;EACN,KAAK;CACP,CAAC;CACD,IAAI,QAAQ,WAAW,GAAG,MAAM,IAAI,MAAM,kCAAkC;CAC5E,OAAO,qBAAqB,SAAS,OAAO;AAC9C"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/csv.ts"],"sourcesContent":["import { parse } from \"csv-parse/sync\";\nimport type { Machine } from \"./machine\";\nimport { instructionsFromRows } from \"./rows\";\nimport type { Instruction } from \"./types\";\n\n/**\n * Parses the instruction CSV. See `instructionsFromRows` for the row rules.\n *\n * @example\n * ```ts\n * const instructions = parseInstructions(csvFileContents, machine);\n * ```\n */\nexport function parseInstructions(source: string, machine: Machine): Instruction[] {\n const records: Record<string, string>[] = parse(source, {\n columns: true,\n skip_empty_lines: true,\n trim: true,\n bom: true,\n });\n if (records.length === 0) throw new Error(\"the CSV has a header but no rows\");\n return instructionsFromRows(records, machine);\n}\n"],"mappings":";;;;;;;;;;;AAaA,SAAgB,kBAAkB,QAAgB,SAAiC;CACjF,MAAM,UAAoC,MAAM,QAAQ;EACtD,SAAS;EACT,kBAAkB;EAClB,MAAM;EACN,KAAK;CACP,CAAC;CACD,IAAI,QAAQ,WAAW,GAAG,MAAM,IAAI,MAAM,kCAAkC;CAC5E,OAAO,qBAAqB,SAAS,OAAO;AAC9C"}
@@ -29,6 +29,12 @@ function parseIronSetting(value, index) {
29
29
  * Checks a parsed machine file and hands back something the renderer can use.
30
30
  * Every failure names the field, because the fix is always an edit to that
31
31
  * line of JSON.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const machine = parseMachine(JSON.parse(machineFileContents));
36
+ * machine.washer.programs; // ["Off", "Cottons", "Wool", ...]
37
+ * ```
32
38
  */
33
39
  function parseMachine(value) {
34
40
  if (typeof value !== "object" || value === null) fail("the file must contain an object");
@@ -56,6 +62,13 @@ function parseMachine(value) {
56
62
  }
57
63
  };
58
64
  }
65
+ /**
66
+ * @example
67
+ * ```ts
68
+ * ironSetting(machine, "2")?.label; // "••"
69
+ * ironSetting(machine, "nope"); // undefined
70
+ * ```
71
+ */
59
72
  function ironSetting(machine, key) {
60
73
  return machine.iron.settings.find((setting) => setting.key === key);
61
74
  }
@@ -64,6 +77,11 @@ function ironSetting(machine, key) {
64
77
  *
65
78
  * There is no key for "do not iron". That is the `ironing` column's job, and
66
79
  * two places to say the same thing is two places to disagree.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * ironSettingKeys(machine); // ["min", "1", "2", "3"]
84
+ * ```
67
85
  */
68
86
  function ironSettingKeys(machine) {
69
87
  return machine.iron.settings.map((setting) => setting.key);
@@ -72,6 +90,12 @@ function ironSettingKeys(machine) {
72
90
  * `40` becomes `40°`; `cold` stays `cold`. The machine file is free to use
73
91
  * whatever word its display shows for the cold wash, so the degree sign is
74
92
  * decided by whether the value is a number rather than by a list of exceptions.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * formatTemperature("40"); // "40°"
97
+ * formatTemperature("cold"); // "cold"
98
+ * ```
75
99
  */
76
100
  function formatTemperature(value) {
77
101
  return /^\d+$/.test(value) ? `${value}°` : value;
@@ -102,6 +126,12 @@ const mixTags = [
102
126
  * Usually one figure. Piles sharing a load agree on every setting, so they
103
127
  * ought to agree on this too — but the chart is data somebody types, and
104
128
  * printing both is more honest than picking the first one and hoping.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * durationsOf([pile, pile]); // "~2:00"
133
+ * durationsOf([pile, { ...pile, duration: "~1:10" }]); // "~2:00 / ~1:10"
134
+ * ```
105
135
  */
106
136
  function durationsOf(group) {
107
137
  return [...new Set(group.map((item) => item.duration).filter((value) => value !== ""))].join(" / ");
@@ -136,7 +166,9 @@ const COLUMNS = [
136
166
  "drying",
137
167
  "colour_group",
138
168
  "mix_tags",
139
- "notes"
169
+ "notes",
170
+ "reference_name",
171
+ "reference_link"
140
172
  ];
141
173
  var RowError = class extends Error {
142
174
  constructor(row, column, message) {
@@ -175,6 +207,11 @@ function boolean(row, column, value) {
175
207
  * programme name fails here rather than producing a PDF that tells you to turn
176
208
  * the dial to a position that does not exist — and so does a chart written for
177
209
  * a different machine than the one you passed.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * const instructions = instructionsFromRows(JSON.parse(jsonFromStorage), machine);
214
+ * ```
178
215
  */
179
216
  function instructionsFromRows(rows, machine) {
180
217
  const { washer } = machine;
@@ -207,11 +244,20 @@ function instructionsFromRows(rows, machine) {
207
244
  drying: record.drying ?? "",
208
245
  colourGroup: oneOf(row, "colour_group", record.colour_group ?? "", colourGroups),
209
246
  mixTags: tags,
210
- notes: record.notes ?? ""
247
+ notes: record.notes ?? "",
248
+ referenceName: record.reference_name ?? "",
249
+ referenceLink: record.reference_link ?? ""
211
250
  };
212
251
  });
213
252
  }
214
- /** The reverse of `instructionsFromRows` — what a JSON export writes out. */
253
+ /**
254
+ * The reverse of `instructionsFromRows` — what a JSON export writes out.
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * const rows = rowsFromInstructions(instructions);
259
+ * ```
260
+ */
215
261
  function rowsFromInstructions(instructions) {
216
262
  return instructions.map((instruction) => ({
217
263
  clothing_type: instruction.clothingType,
@@ -228,18 +274,32 @@ function rowsFromInstructions(instructions) {
228
274
  drying: instruction.drying,
229
275
  colour_group: instruction.colourGroup,
230
276
  mix_tags: instruction.mixTags.join("|"),
231
- notes: instruction.notes
277
+ notes: instruction.notes,
278
+ reference_name: instruction.referenceName,
279
+ reference_link: instruction.referenceLink
232
280
  }));
233
281
  }
234
282
  /**
235
283
  * The JSON interchange format for a chart — the same row shape
236
284
  * a chart's JSON Schema describes, so a file written here validates against
237
285
  * it exactly as a CSV does.
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * const json = chartToJson(instructions);
290
+ * ```
238
291
  */
239
292
  function chartToJson(instructions) {
240
293
  return `${JSON.stringify(rowsFromInstructions(instructions), null, 2)}\n`;
241
294
  }
242
- /** The other half of `chartToJson`. Throws the same errors `parseInstructions` does. */
295
+ /**
296
+ * The other half of `chartToJson`. Throws the same errors `parseInstructions` does.
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * const instructions = chartFromJson(jsonFromStorage, machine);
301
+ * ```
302
+ */
243
303
  function chartFromJson(source, machine) {
244
304
  let rows;
245
305
  try {
@@ -253,10 +313,25 @@ function chartFromJson(source, machine) {
253
313
  //#endregion
254
314
  //#region src/config.ts
255
315
  /**
316
+ * Where `configToJson` points editors at the real JSON Schema (generated
317
+ * from the `Config` type — see `schema/config.schema.json` and `bun run
318
+ * schema`). Unpinned to the package's `latest` npm tag rather than the
319
+ * running version, matching how most `$schema` URLs work: close enough,
320
+ * without needing a build-time read of this package's own `package.json`.
321
+ */
322
+ const CONFIG_SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@washy-washy/core/schema/config.schema.json";
323
+ /**
256
324
  * Validates a `{ machine, chart }` object, checking the chart against the
257
325
  * machine it's embedded with — never a different one. Every failure names
258
326
  * which half is wrong, same as `parseMachine` and `instructionsFromRows` do
259
327
  * on their own.
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * const config = parseConfig(JSON.parse(configFileContents));
332
+ * config.machine.washer.name; // "Generic front loader"
333
+ * config.chart[0].clothingType; // "Dark"
334
+ * ```
260
335
  */
261
336
  function parseConfig(value) {
262
337
  if (typeof value !== "object" || value === null) throw new Error("config: the file must contain an object");
@@ -270,15 +345,34 @@ function parseConfig(value) {
270
345
  chart: instructionsFromRows(raw.chart, machine)
271
346
  };
272
347
  }
273
- /** The reverse of `parseConfig` — what a JSON export writes out. */
348
+ /**
349
+ * The reverse of `parseConfig` — what a JSON export writes out. Leads with a
350
+ * `$schema` key so an editor that reads it (VS Code among them) validates
351
+ * and autocompletes the file without the person editing it doing anything.
352
+ * `parseConfig` ignores the key on the way back in — it never asks for
353
+ * anything beyond `machine` and `chart`.
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * const json = configToJson(config);
358
+ * ```
359
+ */
274
360
  function configToJson(config) {
275
361
  const rows = {
362
+ $schema: CONFIG_SCHEMA_URL,
276
363
  machine: config.machine,
277
364
  chart: rowsFromInstructions(config.chart)
278
365
  };
279
366
  return `${JSON.stringify(rows, null, 2)}\n`;
280
367
  }
281
- /** The other half of `configToJson`. Throws the same errors `parseConfig` does. */
368
+ /**
369
+ * The other half of `configToJson`. Throws the same errors `parseConfig` does.
370
+ *
371
+ * @example
372
+ * ```ts
373
+ * const config = configFromJson(configFileContents);
374
+ * ```
375
+ */
282
376
  function configFromJson(source) {
283
377
  let value;
284
378
  try {
@@ -316,6 +410,12 @@ function compatibleColours(a, b) {
316
410
  * The order matters: the reason reported is the one you would want to hear
317
411
  * first. "Wash it alone" beats "the spin speed differs", because changing the
318
412
  * spin speed would not help.
413
+ *
414
+ * @example
415
+ * ```ts
416
+ * mixBlocker(towels, socks); // "lint" — towels shed onto everything else
417
+ * mixBlocker(socks, socks2); // null — nothing stops them sharing a drum
418
+ * ```
319
419
  */
320
420
  function mixBlocker(a, b) {
321
421
  if (a.mixTags.includes("solo") || b.mixTags.includes("solo")) return "solo";
@@ -324,10 +424,23 @@ function mixBlocker(a, b) {
324
424
  if (!sameSettings(a, b)) return "settings";
325
425
  return null;
326
426
  }
427
+ /**
428
+ * @example
429
+ * ```ts
430
+ * canMix(towels, socks); // false
431
+ * ```
432
+ */
327
433
  function canMix(a, b) {
328
434
  return mixBlocker(a, b) === null;
329
435
  }
330
- /** Annotates each instruction with the other piles it may share a load with. */
436
+ /**
437
+ * Annotates each instruction with the other piles it may share a load with.
438
+ *
439
+ * @example
440
+ * ```ts
441
+ * resolve(instructions)[0].mixesWith; // ["Socks", "Jeans"]
442
+ * ```
443
+ */
331
444
  function resolve(instructions) {
332
445
  return instructions.map((instruction) => ({
333
446
  ...instruction,
@@ -337,6 +450,11 @@ function resolve(instructions) {
337
450
  /**
338
451
  * Groups piles that can all be washed together — every member compatible with
339
452
  * every other, not merely with the first one it met.
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * loadGroups(instructions); // [[towels], [socks, jeans], [wool]]
457
+ * ```
340
458
  */
341
459
  function loadGroups(instructions) {
342
460
  const groups = [];
@@ -377,6 +495,12 @@ function washFingerprint(item) {
377
495
  * Piles you set the machine and the iron up for identically. Every dial
378
496
  * drawing on their cards would be the same drawing, so they get one card
379
497
  * listing all the names.
498
+ *
499
+ * @example
500
+ * ```ts
501
+ * cardGroups(instructions).map((group) => group.map((item) => item.clothingType));
502
+ * // [["Socks", "Jeans"], ["Towels"]]
503
+ * ```
380
504
  */
381
505
  function cardGroups(instructions) {
382
506
  return groupBy(instructions, (item) => JSON.stringify([
@@ -389,6 +513,11 @@ function cardGroups(instructions) {
389
513
  * The same, for a sheet with no iron on it. Dark, Black Socks and Denim each
390
514
  * need their own card on the full chart only because they want three different
391
515
  * thermostat positions; standing at the machine they are one wash.
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * washGroups(instructions); // merges piles cardGroups would have split on iron settings alone
520
+ * ```
392
521
  */
393
522
  function washGroups(instructions) {
394
523
  return groupBy(instructions, washFingerprint);
@@ -402,6 +531,11 @@ function washGroups(instructions) {
402
531
  * and the piles are the list under it. `order` is `ironSettingKeys`; a pile you
403
532
  * never iron has no position in it and sorts last, which is right — it is the
404
533
  * pile you never pick up.
534
+ *
535
+ * @example
536
+ * ```ts
537
+ * ironGroups(instructions, ironSettingKeys(machine));
538
+ * ```
405
539
  */
406
540
  function ironGroups(instructions, order) {
407
541
  const rank = (setting) => {
@@ -411,6 +545,6 @@ function ironGroups(instructions, order) {
411
545
  return groupBy(instructions, (item) => item.ironSetting).sort((a, b) => rank(a[0].ironSetting) - rank(b[0].ironSetting));
412
546
  }
413
547
  //#endregion
414
- export { formatTemperature as C, parseMachine as E, variants as S, ironSettingKeys as T, instructionsFromRows as _, ironGroups as a, durationsOf as b, resolve as c, configToJson as d, parseConfig as f, chartToJson as g, chartFromJson as h, cardGroups as i, washGroups as l, RowError as m, blockerLegend as n, loadGroups as o, COLUMNS as p, canMix as r, mixBlocker as s, blockerCode as t, configFromJson as u, rowsFromInstructions as v, ironSetting as w, mixTags as x, colourGroups as y };
548
+ export { variants as C, parseMachine as D, ironSettingKeys as E, mixTags as S, ironSetting as T, chartToJson as _, ironGroups as a, colourGroups as b, resolve as c, configFromJson as d, configToJson as f, chartFromJson as g, RowError as h, cardGroups as i, washGroups as l, COLUMNS as m, blockerLegend as n, loadGroups as o, parseConfig as p, canMix as r, mixBlocker as s, blockerCode as t, CONFIG_SCHEMA_URL as u, instructionsFromRows as v, formatTemperature as w, durationsOf as x, rowsFromInstructions as y };
415
549
 
416
- //# sourceMappingURL=mixing-DzbYAriI.mjs.map
550
+ //# sourceMappingURL=mixing-Dt043zmh.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mixing-Dt043zmh.mjs","names":[],"sources":["../src/machine.ts","../src/types.ts","../src/rows.ts","../src/config.ts","../src/mixing.ts"],"sourcesContent":["/**\n * The appliances a chart is drawn for, as data rather than code.\n *\n * Everything the PDFs render — dial angles, which chips exist, what a valid CSV\n * value is — comes from a machine file, so pointing this at a different washing\n * machine is editing JSON rather than editing TypeScript. The labels in that\n * file are copied off the fascia in whatever language it is printed in, and\n * nothing here ever translates them: a chart that says \"Cottons\" when the dial\n * says \"Katoen\" is worse than no chart, because you have to translate it back\n * while standing in front of the machine.\n */\n\nexport interface Washer {\n name: string;\n capacity: string;\n /** Dial labels in physical order from twelve o'clock, clockwise. Index is the angle. */\n programs: string[];\n temperatures: string[];\n spins: string[];\n options: string[];\n}\n\nexport interface IronSetting {\n key: string;\n dots: string;\n label: string;\n detail: string;\n steam: boolean;\n}\n\nexport interface Iron {\n name: string;\n /** Thermostat positions in order, coolest first. */\n settings: IronSetting[];\n}\n\nexport interface Machine {\n washer: Washer;\n iron: Iron;\n}\n\nfunction fail(what: string): never {\n throw new Error(`machine: ${what}`);\n}\n\nfunction stringList(value: unknown, field: string, minimum: number): string[] {\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== \"string\" || entry === \"\")) {\n fail(`${field} must be a list of non-empty strings`);\n }\n const list = value as string[];\n if (list.length < minimum)\n fail(`${field} needs at least ${minimum} entries, found ${list.length}`);\n if (new Set(list).size !== list.length) fail(`${field} repeats a value`);\n return list;\n}\n\nfunction text(value: unknown, field: string, fallback?: string): string {\n if (typeof value === \"string\" && value !== \"\") return value;\n if (fallback !== undefined) return fallback;\n return fail(`${field} must be a non-empty string`);\n}\n\nfunction parseIronSetting(value: unknown, index: number): IronSetting {\n if (typeof value !== \"object\" || value === null)\n fail(`iron.settings[${index}] must be an object`);\n const raw = value as Record<string, unknown>;\n const key = text(raw.key, `iron.settings[${index}].key`);\n return {\n key,\n dots: typeof raw.dots === \"string\" ? raw.dots : \"\",\n label: text(raw.label, `iron.settings[${index}].label`),\n detail: text(raw.detail, `iron.settings[${index}].detail`, \"\"),\n steam: raw.steam === true,\n };\n}\n\n/**\n * Checks a parsed machine file and hands back something the renderer can use.\n * Every failure names the field, because the fix is always an edit to that\n * line of JSON.\n *\n * @example\n * ```ts\n * const machine = parseMachine(JSON.parse(machineFileContents));\n * machine.washer.programs; // [\"Off\", \"Cottons\", \"Wool\", ...]\n * ```\n */\nexport function parseMachine(value: unknown): Machine {\n if (typeof value !== \"object\" || value === null) fail(\"the file must contain an object\");\n const raw = value as Record<string, unknown>;\n\n if (typeof raw.washer !== \"object\" || raw.washer === null) fail(\"washer is missing\");\n if (typeof raw.iron !== \"object\" || raw.iron === null) fail(\"iron is missing\");\n\n const washerRaw = raw.washer as Record<string, unknown>;\n const ironRaw = raw.iron as Record<string, unknown>;\n\n const settings = Array.isArray(ironRaw.settings)\n ? ironRaw.settings\n : fail(\"iron.settings is missing\");\n if (settings.length < 2) fail(\"iron.settings needs at least 2 positions to draw a ring\");\n\n const keys = settings.map((setting, index) => parseIronSetting(setting, index));\n if (new Set(keys.map((setting) => setting.key)).size !== keys.length) {\n fail(\"iron.settings repeats a key\");\n }\n\n return {\n washer: {\n name: text(washerRaw.name, \"washer.name\"),\n capacity: text(washerRaw.capacity, \"washer.capacity\", \"\"),\n programs: stringList(washerRaw.programs, \"washer.programs\", 2),\n temperatures: stringList(washerRaw.temperatures, \"washer.temperatures\", 1),\n spins: stringList(washerRaw.spins, \"washer.spins\", 1),\n options: stringList(washerRaw.options ?? [], \"washer.options\", 0),\n },\n iron: {\n name: text(ironRaw.name, \"iron.name\"),\n settings: keys,\n },\n };\n}\n\n/**\n * @example\n * ```ts\n * ironSetting(machine, \"2\")?.label; // \"••\"\n * ironSetting(machine, \"nope\"); // undefined\n * ```\n */\nexport function ironSetting(machine: Machine, key: string): IronSetting | undefined {\n return machine.iron.settings.find((setting) => setting.key === key);\n}\n\n/**\n * The keys a row may write in `iron_setting`, coolest first.\n *\n * There is no key for \"do not iron\". That is the `ironing` column's job, and\n * two places to say the same thing is two places to disagree.\n *\n * @example\n * ```ts\n * ironSettingKeys(machine); // [\"min\", \"1\", \"2\", \"3\"]\n * ```\n */\nexport function ironSettingKeys(machine: Machine): string[] {\n return machine.iron.settings.map((setting) => setting.key);\n}\n\n/**\n * `40` becomes `40°`; `cold` stays `cold`. The machine file is free to use\n * whatever word its display shows for the cold wash, so the degree sign is\n * decided by whether the value is a number rather than by a list of exceptions.\n *\n * @example\n * ```ts\n * formatTemperature(\"40\"); // \"40°\"\n * formatTemperature(\"cold\"); // \"cold\"\n * ```\n */\nexport function formatTemperature(value: string): string {\n return /^\\d+$/.test(value) ? `${value}°` : value;\n}\n","/** Which pile a garment belongs to on colour grounds. `any` mixes with all. */\nexport const colourGroups = [\"white\", \"colour\", \"dark\", \"sport\", \"any\"] as const;\nexport type ColourGroup = (typeof colourGroups)[number];\n\n/**\n * Reasons two piles might not belong together even when the machine settings\n * agree. See `canMix` in `mixing.ts` for how each one is applied.\n */\nexport const mixTags = [\"lint-shedder\", \"lint-magnet\", \"dye-bleeder\", \"solo\"] as const;\nexport type MixTag = (typeof mixTags)[number];\n\n/**\n * One CSV row: everything needed to wash, iron and dry one pile of laundry.\n *\n * The machine-facing fields are plain strings rather than unions, because what\n * counts as valid depends on the machine file you point this at. `csv.ts`\n * checks each of them against that machine as it parses, so nothing downstream\n * has to wonder whether a programme name exists.\n */\nexport interface Instruction {\n clothingType: string;\n detergent: string;\n fabricSoftener: boolean;\n temperature: string;\n spin: string;\n duration: string;\n program: string;\n options: string[];\n /** Whether this pile gets ironed at all. */\n ironing: boolean;\n /**\n * How to iron it, or why you don't. Free prose and often empty, because most\n * of what there is to say about a pile nobody irons is already said by\n * `ironing` being false.\n */\n ironingNotes: string;\n /** Thermostat position. Empty when `ironing` is false. */\n ironSetting: string;\n drying: string;\n colourGroup: ColourGroup;\n mixTags: MixTag[];\n notes: string;\n /**\n * Who to credit for a care instruction that isn't obvious from the garment\n * itself — \"the label says 40°\" doesn't need one, \"the manufacturer says\n * wash these alone\" might. Empty when there's nothing to cite.\n */\n referenceName: string;\n /** A link backing up `referenceName`. Empty when there's nothing to cite. */\n referenceLink: string;\n}\n\n/** An instruction plus the other piles it may share a drum with. */\nexport interface ResolvedInstruction extends Instruction {\n mixesWith: string[];\n}\n\n/**\n * How long a card or a load is going to tie the machine up.\n *\n * Usually one figure. Piles sharing a load agree on every setting, so they\n * ought to agree on this too — but the chart is data somebody types, and\n * printing both is more honest than picking the first one and hoping.\n *\n * @example\n * ```ts\n * durationsOf([pile, pile]); // \"~2:00\"\n * durationsOf([pile, { ...pile, duration: \"~1:10\" }]); // \"~2:00 / ~1:10\"\n * ```\n */\nexport function durationsOf(group: Instruction[]): string {\n return [...new Set(group.map((item) => item.duration).filter((value) => value !== \"\"))].join(\n \" / \",\n );\n}\n\n/**\n * Which sheet to draw.\n *\n * `full` is the whole chart. The other two are the same chart cut where the\n * work is: washing happens in front of the machine on a Sunday morning, ironing\n * happens at a board on a Wednesday evening, and neither job wants to read past\n * the other's advice to find its own.\n */\nexport const variants = [\"full\", \"wash\", \"iron\"] as const;\nexport type Variant = (typeof variants)[number];\n","import { ironSettingKeys, type Machine } from \"./machine\";\nimport type { ColourGroup, Instruction, MixTag } from \"./types\";\nimport { colourGroups, mixTags } from \"./types\";\n\nexport const COLUMNS = [\n \"clothing_type\",\n \"detergent\",\n \"fabric_softener\",\n \"temperature\",\n \"spin\",\n \"duration\",\n \"program\",\n \"options\",\n \"ironing\",\n \"ironing_notes\",\n \"iron_setting\",\n \"drying\",\n \"colour_group\",\n \"mix_tags\",\n \"notes\",\n \"reference_name\",\n \"reference_link\",\n] as const;\n\n/** One row, keyed the same way as a CSV column and the chart's JSON Schema. */\nexport type Row = Record<(typeof COLUMNS)[number], string>;\n\nexport class RowError extends Error {\n constructor(row: number, column: string, message: string) {\n super(`row ${row}, column \"${column}\": ${message}`);\n this.name = \"RowError\";\n }\n}\n\n/** `a|b|c` -> `[\"a\", \"b\", \"c\"]`, tolerating stray whitespace and empties. */\nfunction splitList(value: string): string[] {\n return value\n .split(\"|\")\n .map((part) => part.trim())\n .filter((part) => part.length > 0);\n}\n\nfunction oneOf<T extends string>(\n row: number,\n column: string,\n value: string,\n allowed: readonly T[],\n): T {\n if (!(allowed as readonly string[]).includes(value)) {\n throw new RowError(row, column, `\"${value}\" is not one of ${allowed.join(\", \")}`);\n }\n return value as T;\n}\n\nfunction boolean(row: number, column: string, value: string): boolean {\n const normalised = value.trim().toLowerCase();\n if ([\"yes\", \"y\", \"true\", \"1\"].includes(normalised)) return true;\n if ([\"no\", \"n\", \"false\", \"0\"].includes(normalised)) return false;\n throw new RowError(row, column, `\"${value}\" is not a yes/no value`);\n}\n\n/**\n * Checks rows shaped like the chart's schema — the same shape a CSV parses\n * into, or a JSON file round-tripped through `rowsFromInstructions` — against\n * what the appliances in the machine file can actually be set to. A typo in a\n * programme name fails here rather than producing a PDF that tells you to turn\n * the dial to a position that does not exist — and so does a chart written for\n * a different machine than the one you passed.\n *\n * @example\n * ```ts\n * const instructions = instructionsFromRows(JSON.parse(jsonFromStorage), machine);\n * ```\n */\nexport function instructionsFromRows(\n rows: Record<string, string>[],\n machine: Machine,\n): Instruction[] {\n const { washer } = machine;\n\n if (rows.length === 0) throw new Error(\"the chart has no rows\");\n\n const header = Object.keys(rows[0] as Record<string, string>);\n const missing = COLUMNS.filter((column) => !header.includes(column));\n if (missing.length > 0) {\n throw new Error(`the chart is missing column(s): ${missing.join(\", \")}`);\n }\n\n return rows.map((record, index) => {\n // +2: one for the header row, one because humans count from 1. Kept even\n // for a JSON source, so an error message reads the same regardless of\n // which format the chart came from.\n const row = index + 2;\n\n const clothingType = (record.clothing_type ?? \"\").trim();\n if (clothingType === \"\") throw new RowError(row, \"clothing_type\", \"must not be empty\");\n\n const options = splitList(record.options ?? \"\").map((option) =>\n oneOf(row, \"options\", option, washer.options),\n );\n\n const tags = splitList(record.mix_tags ?? \"\").map((tag) =>\n oneOf<MixTag>(row, \"mix_tags\", tag, mixTags),\n );\n\n // `ironing` decides whether there is a thermostat position at all, so the\n // two are checked together: a pile you never iron has nowhere to point the\n // dial, and a pile you do iron has to say where.\n const ironing = boolean(row, \"ironing\", record.ironing ?? \"\");\n const rawSetting = (record.iron_setting ?? \"\").trim();\n if (!ironing && rawSetting !== \"\") {\n throw new RowError(\n row,\n \"iron_setting\",\n `must be empty when ironing is no, found \"${rawSetting}\"`,\n );\n }\n const ironSetting = ironing\n ? oneOf(row, \"iron_setting\", rawSetting, ironSettingKeys(machine))\n : \"\";\n\n return {\n clothingType,\n detergent: record.detergent ?? \"\",\n fabricSoftener: boolean(row, \"fabric_softener\", record.fabric_softener ?? \"\"),\n temperature: oneOf(row, \"temperature\", record.temperature ?? \"\", washer.temperatures),\n spin: oneOf(row, \"spin\", record.spin ?? \"\", washer.spins),\n duration: record.duration ?? \"\",\n program: oneOf(row, \"program\", record.program ?? \"\", washer.programs),\n options,\n ironing,\n ironingNotes: record.ironing_notes ?? \"\",\n ironSetting,\n drying: record.drying ?? \"\",\n colourGroup: oneOf<ColourGroup>(row, \"colour_group\", record.colour_group ?? \"\", colourGroups),\n mixTags: tags,\n notes: record.notes ?? \"\",\n referenceName: record.reference_name ?? \"\",\n referenceLink: record.reference_link ?? \"\",\n };\n });\n}\n\n/**\n * The reverse of `instructionsFromRows` — what a JSON export writes out.\n *\n * @example\n * ```ts\n * const rows = rowsFromInstructions(instructions);\n * ```\n */\nexport function rowsFromInstructions(instructions: Instruction[]): Row[] {\n return instructions.map((instruction) => ({\n clothing_type: instruction.clothingType,\n detergent: instruction.detergent,\n fabric_softener: instruction.fabricSoftener ? \"yes\" : \"no\",\n temperature: instruction.temperature,\n spin: instruction.spin,\n duration: instruction.duration,\n program: instruction.program,\n options: instruction.options.join(\"|\"),\n ironing: instruction.ironing ? \"yes\" : \"no\",\n ironing_notes: instruction.ironingNotes,\n iron_setting: instruction.ironSetting,\n drying: instruction.drying,\n colour_group: instruction.colourGroup,\n mix_tags: instruction.mixTags.join(\"|\"),\n notes: instruction.notes,\n reference_name: instruction.referenceName,\n reference_link: instruction.referenceLink,\n }));\n}\n\n/**\n * The JSON interchange format for a chart — the same row shape\n * a chart's JSON Schema describes, so a file written here validates against\n * it exactly as a CSV does.\n *\n * @example\n * ```ts\n * const json = chartToJson(instructions);\n * ```\n */\nexport function chartToJson(instructions: Instruction[]): string {\n return `${JSON.stringify(rowsFromInstructions(instructions), null, 2)}\\n`;\n}\n\n/**\n * The other half of `chartToJson`. Throws the same errors `parseInstructions` does.\n *\n * @example\n * ```ts\n * const instructions = chartFromJson(jsonFromStorage, machine);\n * ```\n */\nexport function chartFromJson(source: string, machine: Machine): Instruction[] {\n let rows: unknown;\n try {\n rows = JSON.parse(source);\n } catch (error) {\n throw new Error(`not valid JSON: ${error instanceof Error ? error.message : error}`);\n }\n if (!Array.isArray(rows)) throw new Error(\"the chart must be a JSON array of rows\");\n return instructionsFromRows(rows as Record<string, string>[], machine);\n}\n","import { type Machine, parseMachine } from \"./machine\";\nimport { instructionsFromRows, rowsFromInstructions } from \"./rows\";\nimport type { Instruction } from \"./types\";\n\n/**\n * Where `configToJson` points editors at the real JSON Schema (generated\n * from the `Config` type — see `schema/config.schema.json` and `bun run\n * schema`). Unpinned to the package's `latest` npm tag rather than the\n * running version, matching how most `$schema` URLs work: close enough,\n * without needing a build-time read of this package's own `package.json`.\n */\nexport const CONFIG_SCHEMA_URL =\n \"https://cdn.jsdelivr.net/npm/@washy-washy/core/schema/config.schema.json\";\n\n/**\n * The washing machine and the chart that describes it, in one place. Both\n * halves are exactly what `parseMachine`/`instructionsFromRows` already\n * produce — this is a wrapper around the pair, not a third shape.\n */\nexport interface Config {\n machine: Machine;\n chart: Instruction[];\n}\n\n/**\n * Validates a `{ machine, chart }` object, checking the chart against the\n * machine it's embedded with — never a different one. Every failure names\n * which half is wrong, same as `parseMachine` and `instructionsFromRows` do\n * on their own.\n *\n * @example\n * ```ts\n * const config = parseConfig(JSON.parse(configFileContents));\n * config.machine.washer.name; // \"Generic front loader\"\n * config.chart[0].clothingType; // \"Dark\"\n * ```\n */\nexport function parseConfig(value: unknown): Config {\n if (typeof value !== \"object\" || value === null) {\n throw new Error(\"config: the file must contain an object\");\n }\n const raw = value as Record<string, unknown>;\n\n if (!(\"machine\" in raw)) throw new Error(\"config: machine is missing\");\n if (!(\"chart\" in raw)) throw new Error(\"config: chart is missing\");\n\n const machine = parseMachine(raw.machine);\n\n if (!Array.isArray(raw.chart)) throw new Error(\"config: chart must be an array of rows\");\n const chart = instructionsFromRows(raw.chart as Record<string, string>[], machine);\n\n return { machine, chart };\n}\n\n/**\n * The reverse of `parseConfig` — what a JSON export writes out. Leads with a\n * `$schema` key so an editor that reads it (VS Code among them) validates\n * and autocompletes the file without the person editing it doing anything.\n * `parseConfig` ignores the key on the way back in — it never asks for\n * anything beyond `machine` and `chart`.\n *\n * @example\n * ```ts\n * const json = configToJson(config);\n * ```\n */\nexport function configToJson(config: Config): string {\n const rows = {\n $schema: CONFIG_SCHEMA_URL,\n machine: config.machine,\n chart: rowsFromInstructions(config.chart),\n };\n return `${JSON.stringify(rows, null, 2)}\\n`;\n}\n\n/**\n * The other half of `configToJson`. Throws the same errors `parseConfig` does.\n *\n * @example\n * ```ts\n * const config = configFromJson(configFileContents);\n * ```\n */\nexport function configFromJson(source: string): Config {\n let value: unknown;\n try {\n value = JSON.parse(source);\n } catch (error) {\n throw new Error(`config: not valid JSON: ${error instanceof Error ? error.message : error}`);\n }\n return parseConfig(value);\n}\n","import type { Instruction, ResolvedInstruction } from \"./types\";\n\n/** Why two piles cannot share a drum. `null` means they can. */\nexport type Blocker = \"solo\" | \"settings\" | \"colour\" | \"lint\";\n\nexport const blockerLegend: Record<Blocker, string> = {\n solo: \"Wash on its own\",\n settings: \"Different programme, temperature, spin or options\",\n colour: \"Colours would run into each other\",\n lint: \"One sheds lint onto the other\",\n};\n\n/** Short codes used in the compatibility matrix, so a cell stays one glyph. */\nexport const blockerCode: Record<Blocker, string> = {\n solo: \"S\",\n settings: \"P\",\n colour: \"C\",\n lint: \"L\",\n};\n\nfunction sameSettings(a: Instruction, b: Instruction): boolean {\n return (\n a.program === b.program &&\n a.temperature === b.temperature &&\n a.spin === b.spin &&\n a.options.length === b.options.length &&\n a.options.every((option) => b.options.includes(option))\n );\n}\n\nfunction compatibleColours(a: Instruction, b: Instruction): boolean {\n if (a.colourGroup === \"any\" || b.colourGroup === \"any\") return true;\n return a.colourGroup === b.colourGroup;\n}\n\n/**\n * Decides whether two piles can go in the drum together, and if not, why.\n *\n * The order matters: the reason reported is the one you would want to hear\n * first. \"Wash it alone\" beats \"the spin speed differs\", because changing the\n * spin speed would not help.\n *\n * @example\n * ```ts\n * mixBlocker(towels, socks); // \"lint\" — towels shed onto everything else\n * mixBlocker(socks, socks2); // null — nothing stops them sharing a drum\n * ```\n */\nexport function mixBlocker(a: Instruction, b: Instruction): Blocker | null {\n if (a.mixTags.includes(\"solo\") || b.mixTags.includes(\"solo\")) return \"solo\";\n\n // Terry sheds lint over everything, so towels only ever go with towels.\n const shedders = [a, b].filter((item) => item.mixTags.includes(\"lint-shedder\"));\n if (shedders.length === 1) return \"lint\";\n\n if (!compatibleColours(a, b)) return \"colour\";\n if (!sameSettings(a, b)) return \"settings\";\n return null;\n}\n\n/**\n * @example\n * ```ts\n * canMix(towels, socks); // false\n * ```\n */\nexport function canMix(a: Instruction, b: Instruction): boolean {\n return mixBlocker(a, b) === null;\n}\n\n/**\n * Annotates each instruction with the other piles it may share a load with.\n *\n * @example\n * ```ts\n * resolve(instructions)[0].mixesWith; // [\"Socks\", \"Jeans\"]\n * ```\n */\nexport function resolve(instructions: Instruction[]): ResolvedInstruction[] {\n return instructions.map((instruction) => ({\n ...instruction,\n mixesWith: instructions\n .filter((other) => other !== instruction && canMix(instruction, other))\n .map((other) => other.clothingType),\n }));\n}\n\n/**\n * Groups piles that can all be washed together — every member compatible with\n * every other, not merely with the first one it met.\n *\n * @example\n * ```ts\n * loadGroups(instructions); // [[towels], [socks, jeans], [wool]]\n * ```\n */\nexport function loadGroups<T extends Instruction>(instructions: T[]): T[][] {\n const groups: T[][] = [];\n for (const instruction of instructions) {\n const home = groups.find((group) => group.every((member) => canMix(member, instruction)));\n if (home) home.push(instruction);\n else groups.push([instruction]);\n }\n return groups;\n}\n\nfunction groupBy<T>(items: T[], key: (item: T) => string): T[][] {\n const groups = new Map<string, T[]>();\n for (const item of items) {\n const existing = groups.get(key(item));\n if (existing) existing.push(item);\n else groups.set(key(item), [item]);\n }\n return [...groups.values()];\n}\n\n/**\n * Everything you physically set on the washing machine: the programme, what\n * the display reads, which buttons are lit, and whether softener goes in.\n *\n * Deliberately *not* the prose. Two piles can want the same five settings and\n * still want different detergent or a different drying rack, and those the card\n * lists per pile rather than splitting into two near-identical cards.\n */\nfunction washFingerprint(item: Instruction): string {\n return JSON.stringify([\n item.program,\n item.temperature,\n item.spin,\n [...item.options].sort(),\n item.fabricSoftener,\n ]);\n}\n\n/**\n * Piles you set the machine and the iron up for identically. Every dial\n * drawing on their cards would be the same drawing, so they get one card\n * listing all the names.\n *\n * @example\n * ```ts\n * cardGroups(instructions).map((group) => group.map((item) => item.clothingType));\n * // [[\"Socks\", \"Jeans\"], [\"Towels\"]]\n * ```\n */\nexport function cardGroups<T extends Instruction>(instructions: T[]): T[][] {\n return groupBy(instructions, (item) =>\n JSON.stringify([washFingerprint(item), item.ironing, item.ironSetting]),\n );\n}\n\n/**\n * The same, for a sheet with no iron on it. Dark, Black Socks and Denim each\n * need their own card on the full chart only because they want three different\n * thermostat positions; standing at the machine they are one wash.\n *\n * @example\n * ```ts\n * washGroups(instructions); // merges piles cardGroups would have split on iron settings alone\n * ```\n */\nexport function washGroups<T extends Instruction>(instructions: T[]): T[][] {\n return groupBy(instructions, washFingerprint);\n}\n\n/**\n * Piles by where the iron's thermostat points, coolest first.\n *\n * An ironing sheet is read the other way round from a washing one. You do not\n * fetch a pile and look up its setting — you set the iron once and work through\n * everything that goes at that heat, so the thermostat position is the heading\n * and the piles are the list under it. `order` is `ironSettingKeys`; a pile you\n * never iron has no position in it and sorts last, which is right — it is the\n * pile you never pick up.\n *\n * @example\n * ```ts\n * ironGroups(instructions, ironSettingKeys(machine));\n * ```\n */\nexport function ironGroups<T extends Instruction>(\n instructions: T[],\n order: readonly string[],\n): T[][] {\n const rank = (setting: string) => {\n const at = order.indexOf(setting);\n return at < 0 ? order.length : at;\n };\n return groupBy(instructions, (item) => item.ironSetting).sort(\n (a, b) => rank((a[0] as T).ironSetting) - rank((b[0] as T).ironSetting),\n );\n}\n"],"mappings":"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"}
@@ -39,20 +39,44 @@ interface Machine {
39
39
  * Checks a parsed machine file and hands back something the renderer can use.
40
40
  * Every failure names the field, because the fix is always an edit to that
41
41
  * line of JSON.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const machine = parseMachine(JSON.parse(machineFileContents));
46
+ * machine.washer.programs; // ["Off", "Cottons", "Wool", ...]
47
+ * ```
42
48
  */
43
49
  declare function parseMachine(value: unknown): Machine;
50
+ /**
51
+ * @example
52
+ * ```ts
53
+ * ironSetting(machine, "2")?.label; // "••"
54
+ * ironSetting(machine, "nope"); // undefined
55
+ * ```
56
+ */
44
57
  declare function ironSetting(machine: Machine, key: string): IronSetting | undefined;
45
58
  /**
46
59
  * The keys a row may write in `iron_setting`, coolest first.
47
60
  *
48
61
  * There is no key for "do not iron". That is the `ironing` column's job, and
49
62
  * two places to say the same thing is two places to disagree.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * ironSettingKeys(machine); // ["min", "1", "2", "3"]
67
+ * ```
50
68
  */
51
69
  declare function ironSettingKeys(machine: Machine): string[];
52
70
  /**
53
71
  * `40` becomes `40°`; `cold` stays `cold`. The machine file is free to use
54
72
  * whatever word its display shows for the cold wash, so the degree sign is
55
73
  * decided by whether the value is a number rather than by a list of exceptions.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * formatTemperature("40"); // "40°"
78
+ * formatTemperature("cold"); // "cold"
79
+ * ```
56
80
  */
57
81
  declare function formatTemperature(value: string): string;
58
82
  //#endregion
@@ -97,6 +121,14 @@ interface Instruction {
97
121
  colourGroup: ColourGroup;
98
122
  mixTags: MixTag[];
99
123
  notes: string;
124
+ /**
125
+ * Who to credit for a care instruction that isn't obvious from the garment
126
+ * itself — "the label says 40°" doesn't need one, "the manufacturer says
127
+ * wash these alone" might. Empty when there's nothing to cite.
128
+ */
129
+ referenceName: string;
130
+ /** A link backing up `referenceName`. Empty when there's nothing to cite. */
131
+ referenceLink: string;
100
132
  }
101
133
  /** An instruction plus the other piles it may share a drum with. */
102
134
  interface ResolvedInstruction extends Instruction {
@@ -108,6 +140,12 @@ interface ResolvedInstruction extends Instruction {
108
140
  * Usually one figure. Piles sharing a load agree on every setting, so they
109
141
  * ought to agree on this too — but the chart is data somebody types, and
110
142
  * printing both is more honest than picking the first one and hoping.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * durationsOf([pile, pile]); // "~2:00"
147
+ * durationsOf([pile, { ...pile, duration: "~1:10" }]); // "~2:00 / ~1:10"
148
+ * ```
111
149
  */
112
150
  declare function durationsOf(group: Instruction[]): string;
113
151
  /**
@@ -122,6 +160,14 @@ declare const variants: readonly ["full", "wash", "iron"];
122
160
  type Variant = (typeof variants)[number];
123
161
  //#endregion
124
162
  //#region src/config.d.ts
163
+ /**
164
+ * Where `configToJson` points editors at the real JSON Schema (generated
165
+ * from the `Config` type — see `schema/config.schema.json` and `bun run
166
+ * schema`). Unpinned to the package's `latest` npm tag rather than the
167
+ * running version, matching how most `$schema` URLs work: close enough,
168
+ * without needing a build-time read of this package's own `package.json`.
169
+ */
170
+ declare const CONFIG_SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@washy-washy/core/schema/config.schema.json";
125
171
  /**
126
172
  * The washing machine and the chart that describes it, in one place. Both
127
173
  * halves are exactly what `parseMachine`/`instructionsFromRows` already
@@ -136,11 +182,36 @@ interface Config {
136
182
  * machine it's embedded with — never a different one. Every failure names
137
183
  * which half is wrong, same as `parseMachine` and `instructionsFromRows` do
138
184
  * on their own.
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * const config = parseConfig(JSON.parse(configFileContents));
189
+ * config.machine.washer.name; // "Generic front loader"
190
+ * config.chart[0].clothingType; // "Dark"
191
+ * ```
139
192
  */
140
193
  declare function parseConfig(value: unknown): Config;
141
- /** The reverse of `parseConfig` — what a JSON export writes out. */
194
+ /**
195
+ * The reverse of `parseConfig` — what a JSON export writes out. Leads with a
196
+ * `$schema` key so an editor that reads it (VS Code among them) validates
197
+ * and autocompletes the file without the person editing it doing anything.
198
+ * `parseConfig` ignores the key on the way back in — it never asks for
199
+ * anything beyond `machine` and `chart`.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * const json = configToJson(config);
204
+ * ```
205
+ */
142
206
  declare function configToJson(config: Config): string;
143
- /** The other half of `configToJson`. Throws the same errors `parseConfig` does. */
207
+ /**
208
+ * The other half of `configToJson`. Throws the same errors `parseConfig` does.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const config = configFromJson(configFileContents);
213
+ * ```
214
+ */
144
215
  declare function configFromJson(source: string): Config;
145
216
  //#endregion
146
217
  //#region src/mixing.d.ts
@@ -155,26 +226,61 @@ declare const blockerCode: Record<Blocker, string>;
155
226
  * The order matters: the reason reported is the one you would want to hear
156
227
  * first. "Wash it alone" beats "the spin speed differs", because changing the
157
228
  * spin speed would not help.
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * mixBlocker(towels, socks); // "lint" — towels shed onto everything else
233
+ * mixBlocker(socks, socks2); // null — nothing stops them sharing a drum
234
+ * ```
158
235
  */
159
236
  declare function mixBlocker(a: Instruction, b: Instruction): Blocker | null;
237
+ /**
238
+ * @example
239
+ * ```ts
240
+ * canMix(towels, socks); // false
241
+ * ```
242
+ */
160
243
  declare function canMix(a: Instruction, b: Instruction): boolean;
161
- /** Annotates each instruction with the other piles it may share a load with. */
244
+ /**
245
+ * Annotates each instruction with the other piles it may share a load with.
246
+ *
247
+ * @example
248
+ * ```ts
249
+ * resolve(instructions)[0].mixesWith; // ["Socks", "Jeans"]
250
+ * ```
251
+ */
162
252
  declare function resolve(instructions: Instruction[]): ResolvedInstruction[];
163
253
  /**
164
254
  * Groups piles that can all be washed together — every member compatible with
165
255
  * every other, not merely with the first one it met.
256
+ *
257
+ * @example
258
+ * ```ts
259
+ * loadGroups(instructions); // [[towels], [socks, jeans], [wool]]
260
+ * ```
166
261
  */
167
262
  declare function loadGroups<T extends Instruction>(instructions: T[]): T[][];
168
263
  /**
169
264
  * Piles you set the machine and the iron up for identically. Every dial
170
265
  * drawing on their cards would be the same drawing, so they get one card
171
266
  * listing all the names.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * cardGroups(instructions).map((group) => group.map((item) => item.clothingType));
271
+ * // [["Socks", "Jeans"], ["Towels"]]
272
+ * ```
172
273
  */
173
274
  declare function cardGroups<T extends Instruction>(instructions: T[]): T[][];
174
275
  /**
175
276
  * The same, for a sheet with no iron on it. Dark, Black Socks and Denim each
176
277
  * need their own card on the full chart only because they want three different
177
278
  * thermostat positions; standing at the machine they are one wash.
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * washGroups(instructions); // merges piles cardGroups would have split on iron settings alone
283
+ * ```
178
284
  */
179
285
  declare function washGroups<T extends Instruction>(instructions: T[]): T[][];
180
286
  /**
@@ -186,11 +292,16 @@ declare function washGroups<T extends Instruction>(instructions: T[]): T[][];
186
292
  * and the piles are the list under it. `order` is `ironSettingKeys`; a pile you
187
293
  * never iron has no position in it and sorts last, which is right — it is the
188
294
  * pile you never pick up.
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * ironGroups(instructions, ironSettingKeys(machine));
299
+ * ```
189
300
  */
190
301
  declare function ironGroups<T extends Instruction>(instructions: T[], order: readonly string[]): T[][];
191
302
  //#endregion
192
303
  //#region src/rows.d.ts
193
- declare const COLUMNS: readonly ["clothing_type", "detergent", "fabric_softener", "temperature", "spin", "duration", "program", "options", "ironing", "ironing_notes", "iron_setting", "drying", "colour_group", "mix_tags", "notes"];
304
+ declare const COLUMNS: readonly ["clothing_type", "detergent", "fabric_softener", "temperature", "spin", "duration", "program", "options", "ironing", "ironing_notes", "iron_setting", "drying", "colour_group", "mix_tags", "notes", "reference_name", "reference_link"];
194
305
  /** One row, keyed the same way as a CSV column and the chart's JSON Schema. */
195
306
  type Row = Record<(typeof COLUMNS)[number], string>;
196
307
  declare class RowError extends Error {
@@ -203,18 +314,42 @@ declare class RowError extends Error {
203
314
  * programme name fails here rather than producing a PDF that tells you to turn
204
315
  * the dial to a position that does not exist — and so does a chart written for
205
316
  * a different machine than the one you passed.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * const instructions = instructionsFromRows(JSON.parse(jsonFromStorage), machine);
321
+ * ```
206
322
  */
207
323
  declare function instructionsFromRows(rows: Record<string, string>[], machine: Machine): Instruction[];
208
- /** The reverse of `instructionsFromRows` — what a JSON export writes out. */
324
+ /**
325
+ * The reverse of `instructionsFromRows` — what a JSON export writes out.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * const rows = rowsFromInstructions(instructions);
330
+ * ```
331
+ */
209
332
  declare function rowsFromInstructions(instructions: Instruction[]): Row[];
210
333
  /**
211
334
  * The JSON interchange format for a chart — the same row shape
212
335
  * a chart's JSON Schema describes, so a file written here validates against
213
336
  * it exactly as a CSV does.
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * const json = chartToJson(instructions);
341
+ * ```
214
342
  */
215
343
  declare function chartToJson(instructions: Instruction[]): string;
216
- /** The other half of `chartToJson`. Throws the same errors `parseInstructions` does. */
344
+ /**
345
+ * The other half of `chartToJson`. Throws the same errors `parseInstructions` does.
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * const instructions = chartFromJson(jsonFromStorage, machine);
350
+ * ```
351
+ */
217
352
  declare function chartFromJson(source: string, machine: Machine): Instruction[];
218
353
  //#endregion
219
- export { variants as A, Instruction as C, colourGroups as D, Variant as E, formatTemperature as F, ironSetting as I, ironSettingKeys as L, IronSetting as M, Machine as N, durationsOf as O, Washer as P, parseMachine as R, ColourGroup as S, ResolvedInstruction as T, washGroups as _, chartToJson as a, configToJson as b, Blocker as c, canMix as d, cardGroups as f, resolve as g, mixBlocker as h, chartFromJson as i, Iron as j, mixTags as k, blockerCode as l, loadGroups as m, Row as n, instructionsFromRows as o, ironGroups as p, RowError as r, rowsFromInstructions as s, COLUMNS as t, blockerLegend as u, Config as v, MixTag as w, parseConfig as x, configFromJson as y };
220
- //# sourceMappingURL=rows-C-0YQE-w.d.mts.map
354
+ export { mixTags as A, ColourGroup as C, Variant as D, ResolvedInstruction as E, Washer as F, formatTemperature as I, ironSetting as L, Iron as M, IronSetting as N, colourGroups as O, Machine as P, ironSettingKeys as R, parseConfig as S, MixTag as T, washGroups as _, chartToJson as a, configFromJson as b, Blocker as c, canMix as d, cardGroups as f, resolve as g, mixBlocker as h, chartFromJson as i, variants as j, durationsOf as k, blockerCode as l, loadGroups as m, Row as n, instructionsFromRows as o, ironGroups as p, RowError as r, rowsFromInstructions as s, COLUMNS as t, blockerLegend as u, CONFIG_SCHEMA_URL as v, Instruction as w, configToJson as x, Config as y, parseMachine as z };
355
+ //# sourceMappingURL=rows-Bn1LlIaD.d.mts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@washy-washy/core",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Chart parsing, mixing rules and machine validation for washy-washy — a laundry chart generator.",
5
5
  "keywords": [
6
6
  "laundry",
@@ -27,12 +27,14 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
+ "schema",
30
31
  "README.md",
31
32
  "LICENSE"
32
33
  ],
33
34
  "scripts": {
34
35
  "build": "tsdown",
35
- "check": "bun run lint && bun run lint:md && bun run lint:yaml && bun run lint:package-json && bun run lint:prose && bun run typecheck && bun test",
36
+ "check": "bun run lint && bun run lint:md && bun run lint:yaml && bun run lint:package-json && bun run lint:prose && bun run lint:schema && bun run typecheck && bun test",
37
+ "docs": "typedoc",
36
38
  "format": "prettier --write .",
37
39
  "lint": "biome check .",
38
40
  "lint:fix": "biome check --write .",
@@ -40,9 +42,11 @@
40
42
  "lint:package-json": "sort-package-json --check",
41
43
  "lint:prose": ".tools/vale --glob='!{styles/**,CHANGELOG.md}' --minAlertLevel=error .",
42
44
  "lint:prose:advice": ".tools/vale --glob='!{styles/**,CHANGELOG.md}' .",
45
+ "lint:schema": "bun run schema && git diff --exit-code schema/",
43
46
  "lint:yaml": "prettier --check \"**/*.{yml,yaml}\"",
44
47
  "prepare": "lefthook install",
45
48
  "prose:sync": "bun run scripts/install-vale.ts && .tools/vale sync",
49
+ "schema": "ts-json-schema-generator --path src/config.ts --type Config --tsconfig tsconfig.json -o schema/config.schema.json && biome format --write schema/config.schema.json",
46
50
  "test": "bun test",
47
51
  "typecheck": "tsc --noEmit"
48
52
  },
@@ -65,7 +69,9 @@
65
69
  "prettier": "3.9.6",
66
70
  "semantic-release": "25.0.9",
67
71
  "sort-package-json": "4.0.0",
72
+ "ts-json-schema-generator": "2.9.0",
68
73
  "tsdown": "0.22.14",
74
+ "typedoc": "0.28.20",
69
75
  "typescript": "5.9.3"
70
76
  },
71
77
  "engines": {
@@ -0,0 +1,210 @@
1
+ {
2
+ "$ref": "#/definitions/Config",
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "definitions": {
5
+ "ColourGroup": {
6
+ "enum": ["white", "colour", "dark", "sport", "any"],
7
+ "type": "string"
8
+ },
9
+ "Config": {
10
+ "additionalProperties": false,
11
+ "description": "The washing machine and the chart that describes it, in one place. Both halves are exactly what `parseMachine`/`instructionsFromRows` already produce — this is a wrapper around the pair, not a third shape.",
12
+ "properties": {
13
+ "chart": {
14
+ "items": {
15
+ "$ref": "#/definitions/Instruction"
16
+ },
17
+ "type": "array"
18
+ },
19
+ "machine": {
20
+ "$ref": "#/definitions/Machine"
21
+ }
22
+ },
23
+ "required": ["machine", "chart"],
24
+ "type": "object"
25
+ },
26
+ "Instruction": {
27
+ "additionalProperties": false,
28
+ "description": "One CSV row: everything needed to wash, iron and dry one pile of laundry.\n\nThe machine-facing fields are plain strings rather than unions, because what counts as valid depends on the machine file you point this at. `csv.ts` checks each of them against that machine as it parses, so nothing downstream has to wonder whether a programme name exists.",
29
+ "properties": {
30
+ "clothingType": {
31
+ "type": "string"
32
+ },
33
+ "colourGroup": {
34
+ "$ref": "#/definitions/ColourGroup"
35
+ },
36
+ "detergent": {
37
+ "type": "string"
38
+ },
39
+ "drying": {
40
+ "type": "string"
41
+ },
42
+ "duration": {
43
+ "type": "string"
44
+ },
45
+ "fabricSoftener": {
46
+ "type": "boolean"
47
+ },
48
+ "ironSetting": {
49
+ "description": "Thermostat position. Empty when `ironing` is false.",
50
+ "type": "string"
51
+ },
52
+ "ironing": {
53
+ "description": "Whether this pile gets ironed at all.",
54
+ "type": "boolean"
55
+ },
56
+ "ironingNotes": {
57
+ "description": "How to iron it, or why you don't. Free prose and often empty, because most of what there is to say about a pile nobody irons is already said by `ironing` being false.",
58
+ "type": "string"
59
+ },
60
+ "mixTags": {
61
+ "items": {
62
+ "$ref": "#/definitions/MixTag"
63
+ },
64
+ "type": "array"
65
+ },
66
+ "notes": {
67
+ "type": "string"
68
+ },
69
+ "options": {
70
+ "items": {
71
+ "type": "string"
72
+ },
73
+ "type": "array"
74
+ },
75
+ "program": {
76
+ "type": "string"
77
+ },
78
+ "referenceLink": {
79
+ "description": "A link backing up `referenceName`. Empty when there's nothing to cite.",
80
+ "type": "string"
81
+ },
82
+ "referenceName": {
83
+ "description": "Who to credit for a care instruction that isn't obvious from the garment itself — \"the label says 40°\" doesn't need one, \"the manufacturer says wash these alone\" might. Empty when there's nothing to cite.",
84
+ "type": "string"
85
+ },
86
+ "spin": {
87
+ "type": "string"
88
+ },
89
+ "temperature": {
90
+ "type": "string"
91
+ }
92
+ },
93
+ "required": [
94
+ "clothingType",
95
+ "detergent",
96
+ "fabricSoftener",
97
+ "temperature",
98
+ "spin",
99
+ "duration",
100
+ "program",
101
+ "options",
102
+ "ironing",
103
+ "ironingNotes",
104
+ "ironSetting",
105
+ "drying",
106
+ "colourGroup",
107
+ "mixTags",
108
+ "notes",
109
+ "referenceName",
110
+ "referenceLink"
111
+ ],
112
+ "type": "object"
113
+ },
114
+ "Iron": {
115
+ "additionalProperties": false,
116
+ "properties": {
117
+ "name": {
118
+ "type": "string"
119
+ },
120
+ "settings": {
121
+ "description": "Thermostat positions in order, coolest first.",
122
+ "items": {
123
+ "$ref": "#/definitions/IronSetting"
124
+ },
125
+ "type": "array"
126
+ }
127
+ },
128
+ "required": ["name", "settings"],
129
+ "type": "object"
130
+ },
131
+ "IronSetting": {
132
+ "additionalProperties": false,
133
+ "properties": {
134
+ "detail": {
135
+ "type": "string"
136
+ },
137
+ "dots": {
138
+ "type": "string"
139
+ },
140
+ "key": {
141
+ "type": "string"
142
+ },
143
+ "label": {
144
+ "type": "string"
145
+ },
146
+ "steam": {
147
+ "type": "boolean"
148
+ }
149
+ },
150
+ "required": ["key", "dots", "label", "detail", "steam"],
151
+ "type": "object"
152
+ },
153
+ "Machine": {
154
+ "additionalProperties": false,
155
+ "properties": {
156
+ "iron": {
157
+ "$ref": "#/definitions/Iron"
158
+ },
159
+ "washer": {
160
+ "$ref": "#/definitions/Washer"
161
+ }
162
+ },
163
+ "required": ["washer", "iron"],
164
+ "type": "object"
165
+ },
166
+ "MixTag": {
167
+ "enum": ["lint-shedder", "lint-magnet", "dye-bleeder", "solo"],
168
+ "type": "string"
169
+ },
170
+ "Washer": {
171
+ "additionalProperties": false,
172
+ "description": "The appliances a chart is drawn for, as data rather than code.\n\nEverything the PDFs render — dial angles, which chips exist, what a valid CSV value is — comes from a machine file, so pointing this at a different washing machine is editing JSON rather than editing TypeScript. The labels in that file are copied off the fascia in whatever language it is printed in, and nothing here ever translates them: a chart that says \"Cottons\" when the dial says \"Katoen\" is worse than no chart, because you have to translate it back while standing in front of the machine.",
173
+ "properties": {
174
+ "capacity": {
175
+ "type": "string"
176
+ },
177
+ "name": {
178
+ "type": "string"
179
+ },
180
+ "options": {
181
+ "items": {
182
+ "type": "string"
183
+ },
184
+ "type": "array"
185
+ },
186
+ "programs": {
187
+ "description": "Dial labels in physical order from twelve o'clock, clockwise. Index is the angle.",
188
+ "items": {
189
+ "type": "string"
190
+ },
191
+ "type": "array"
192
+ },
193
+ "spins": {
194
+ "items": {
195
+ "type": "string"
196
+ },
197
+ "type": "array"
198
+ },
199
+ "temperatures": {
200
+ "items": {
201
+ "type": "string"
202
+ },
203
+ "type": "array"
204
+ }
205
+ },
206
+ "required": ["name", "capacity", "programs", "temperatures", "spins", "options"],
207
+ "type": "object"
208
+ }
209
+ }
210
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"mixing-DzbYAriI.mjs","names":[],"sources":["../src/machine.ts","../src/types.ts","../src/rows.ts","../src/config.ts","../src/mixing.ts"],"sourcesContent":["/**\n * The appliances a chart is drawn for, as data rather than code.\n *\n * Everything the PDFs render — dial angles, which chips exist, what a valid CSV\n * value is — comes from a machine file, so pointing this at a different washing\n * machine is editing JSON rather than editing TypeScript. The labels in that\n * file are copied off the fascia in whatever language it is printed in, and\n * nothing here ever translates them: a chart that says \"Cottons\" when the dial\n * says \"Katoen\" is worse than no chart, because you have to translate it back\n * while standing in front of the machine.\n */\n\nexport interface Washer {\n name: string;\n capacity: string;\n /** Dial labels in physical order from twelve o'clock, clockwise. Index is the angle. */\n programs: string[];\n temperatures: string[];\n spins: string[];\n options: string[];\n}\n\nexport interface IronSetting {\n key: string;\n dots: string;\n label: string;\n detail: string;\n steam: boolean;\n}\n\nexport interface Iron {\n name: string;\n /** Thermostat positions in order, coolest first. */\n settings: IronSetting[];\n}\n\nexport interface Machine {\n washer: Washer;\n iron: Iron;\n}\n\nfunction fail(what: string): never {\n throw new Error(`machine: ${what}`);\n}\n\nfunction stringList(value: unknown, field: string, minimum: number): string[] {\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== \"string\" || entry === \"\")) {\n fail(`${field} must be a list of non-empty strings`);\n }\n const list = value as string[];\n if (list.length < minimum)\n fail(`${field} needs at least ${minimum} entries, found ${list.length}`);\n if (new Set(list).size !== list.length) fail(`${field} repeats a value`);\n return list;\n}\n\nfunction text(value: unknown, field: string, fallback?: string): string {\n if (typeof value === \"string\" && value !== \"\") return value;\n if (fallback !== undefined) return fallback;\n return fail(`${field} must be a non-empty string`);\n}\n\nfunction parseIronSetting(value: unknown, index: number): IronSetting {\n if (typeof value !== \"object\" || value === null)\n fail(`iron.settings[${index}] must be an object`);\n const raw = value as Record<string, unknown>;\n const key = text(raw.key, `iron.settings[${index}].key`);\n return {\n key,\n dots: typeof raw.dots === \"string\" ? raw.dots : \"\",\n label: text(raw.label, `iron.settings[${index}].label`),\n detail: text(raw.detail, `iron.settings[${index}].detail`, \"\"),\n steam: raw.steam === true,\n };\n}\n\n/**\n * Checks a parsed machine file and hands back something the renderer can use.\n * Every failure names the field, because the fix is always an edit to that\n * line of JSON.\n */\nexport function parseMachine(value: unknown): Machine {\n if (typeof value !== \"object\" || value === null) fail(\"the file must contain an object\");\n const raw = value as Record<string, unknown>;\n\n if (typeof raw.washer !== \"object\" || raw.washer === null) fail(\"washer is missing\");\n if (typeof raw.iron !== \"object\" || raw.iron === null) fail(\"iron is missing\");\n\n const washerRaw = raw.washer as Record<string, unknown>;\n const ironRaw = raw.iron as Record<string, unknown>;\n\n const settings = Array.isArray(ironRaw.settings)\n ? ironRaw.settings\n : fail(\"iron.settings is missing\");\n if (settings.length < 2) fail(\"iron.settings needs at least 2 positions to draw a ring\");\n\n const keys = settings.map((setting, index) => parseIronSetting(setting, index));\n if (new Set(keys.map((setting) => setting.key)).size !== keys.length) {\n fail(\"iron.settings repeats a key\");\n }\n\n return {\n washer: {\n name: text(washerRaw.name, \"washer.name\"),\n capacity: text(washerRaw.capacity, \"washer.capacity\", \"\"),\n programs: stringList(washerRaw.programs, \"washer.programs\", 2),\n temperatures: stringList(washerRaw.temperatures, \"washer.temperatures\", 1),\n spins: stringList(washerRaw.spins, \"washer.spins\", 1),\n options: stringList(washerRaw.options ?? [], \"washer.options\", 0),\n },\n iron: {\n name: text(ironRaw.name, \"iron.name\"),\n settings: keys,\n },\n };\n}\n\nexport function ironSetting(machine: Machine, key: string): IronSetting | undefined {\n return machine.iron.settings.find((setting) => setting.key === key);\n}\n\n/**\n * The keys a row may write in `iron_setting`, coolest first.\n *\n * There is no key for \"do not iron\". That is the `ironing` column's job, and\n * two places to say the same thing is two places to disagree.\n */\nexport function ironSettingKeys(machine: Machine): string[] {\n return machine.iron.settings.map((setting) => setting.key);\n}\n\n/**\n * `40` becomes `40°`; `cold` stays `cold`. The machine file is free to use\n * whatever word its display shows for the cold wash, so the degree sign is\n * decided by whether the value is a number rather than by a list of exceptions.\n */\nexport function formatTemperature(value: string): string {\n return /^\\d+$/.test(value) ? `${value}°` : value;\n}\n","/** Which pile a garment belongs to on colour grounds. `any` mixes with all. */\nexport const colourGroups = [\"white\", \"colour\", \"dark\", \"sport\", \"any\"] as const;\nexport type ColourGroup = (typeof colourGroups)[number];\n\n/**\n * Reasons two piles might not belong together even when the machine settings\n * agree. See `canMix` in `mixing.ts` for how each one is applied.\n */\nexport const mixTags = [\"lint-shedder\", \"lint-magnet\", \"dye-bleeder\", \"solo\"] as const;\nexport type MixTag = (typeof mixTags)[number];\n\n/**\n * One CSV row: everything needed to wash, iron and dry one pile of laundry.\n *\n * The machine-facing fields are plain strings rather than unions, because what\n * counts as valid depends on the machine file you point this at. `csv.ts`\n * checks each of them against that machine as it parses, so nothing downstream\n * has to wonder whether a programme name exists.\n */\nexport interface Instruction {\n clothingType: string;\n detergent: string;\n fabricSoftener: boolean;\n temperature: string;\n spin: string;\n duration: string;\n program: string;\n options: string[];\n /** Whether this pile gets ironed at all. */\n ironing: boolean;\n /**\n * How to iron it, or why you don't. Free prose and often empty, because most\n * of what there is to say about a pile nobody irons is already said by\n * `ironing` being false.\n */\n ironingNotes: string;\n /** Thermostat position. Empty when `ironing` is false. */\n ironSetting: string;\n drying: string;\n colourGroup: ColourGroup;\n mixTags: MixTag[];\n notes: string;\n}\n\n/** An instruction plus the other piles it may share a drum with. */\nexport interface ResolvedInstruction extends Instruction {\n mixesWith: string[];\n}\n\n/**\n * How long a card or a load is going to tie the machine up.\n *\n * Usually one figure. Piles sharing a load agree on every setting, so they\n * ought to agree on this too — but the chart is data somebody types, and\n * printing both is more honest than picking the first one and hoping.\n */\nexport function durationsOf(group: Instruction[]): string {\n return [...new Set(group.map((item) => item.duration).filter((value) => value !== \"\"))].join(\n \" / \",\n );\n}\n\n/**\n * Which sheet to draw.\n *\n * `full` is the whole chart. The other two are the same chart cut where the\n * work is: washing happens in front of the machine on a Sunday morning, ironing\n * happens at a board on a Wednesday evening, and neither job wants to read past\n * the other's advice to find its own.\n */\nexport const variants = [\"full\", \"wash\", \"iron\"] as const;\nexport type Variant = (typeof variants)[number];\n","import { ironSettingKeys, type Machine } from \"./machine\";\nimport type { ColourGroup, Instruction, MixTag } from \"./types\";\nimport { colourGroups, mixTags } from \"./types\";\n\nexport const COLUMNS = [\n \"clothing_type\",\n \"detergent\",\n \"fabric_softener\",\n \"temperature\",\n \"spin\",\n \"duration\",\n \"program\",\n \"options\",\n \"ironing\",\n \"ironing_notes\",\n \"iron_setting\",\n \"drying\",\n \"colour_group\",\n \"mix_tags\",\n \"notes\",\n] as const;\n\n/** One row, keyed the same way as a CSV column and the chart's JSON Schema. */\nexport type Row = Record<(typeof COLUMNS)[number], string>;\n\nexport class RowError extends Error {\n constructor(row: number, column: string, message: string) {\n super(`row ${row}, column \"${column}\": ${message}`);\n this.name = \"RowError\";\n }\n}\n\n/** `a|b|c` -> `[\"a\", \"b\", \"c\"]`, tolerating stray whitespace and empties. */\nfunction splitList(value: string): string[] {\n return value\n .split(\"|\")\n .map((part) => part.trim())\n .filter((part) => part.length > 0);\n}\n\nfunction oneOf<T extends string>(\n row: number,\n column: string,\n value: string,\n allowed: readonly T[],\n): T {\n if (!(allowed as readonly string[]).includes(value)) {\n throw new RowError(row, column, `\"${value}\" is not one of ${allowed.join(\", \")}`);\n }\n return value as T;\n}\n\nfunction boolean(row: number, column: string, value: string): boolean {\n const normalised = value.trim().toLowerCase();\n if ([\"yes\", \"y\", \"true\", \"1\"].includes(normalised)) return true;\n if ([\"no\", \"n\", \"false\", \"0\"].includes(normalised)) return false;\n throw new RowError(row, column, `\"${value}\" is not a yes/no value`);\n}\n\n/**\n * Checks rows shaped like the chart's schema — the same shape a CSV parses\n * into, or a JSON file round-tripped through `rowsFromInstructions` — against\n * what the appliances in the machine file can actually be set to. A typo in a\n * programme name fails here rather than producing a PDF that tells you to turn\n * the dial to a position that does not exist — and so does a chart written for\n * a different machine than the one you passed.\n */\nexport function instructionsFromRows(\n rows: Record<string, string>[],\n machine: Machine,\n): Instruction[] {\n const { washer } = machine;\n\n if (rows.length === 0) throw new Error(\"the chart has no rows\");\n\n const header = Object.keys(rows[0] as Record<string, string>);\n const missing = COLUMNS.filter((column) => !header.includes(column));\n if (missing.length > 0) {\n throw new Error(`the chart is missing column(s): ${missing.join(\", \")}`);\n }\n\n return rows.map((record, index) => {\n // +2: one for the header row, one because humans count from 1. Kept even\n // for a JSON source, so an error message reads the same regardless of\n // which format the chart came from.\n const row = index + 2;\n\n const clothingType = (record.clothing_type ?? \"\").trim();\n if (clothingType === \"\") throw new RowError(row, \"clothing_type\", \"must not be empty\");\n\n const options = splitList(record.options ?? \"\").map((option) =>\n oneOf(row, \"options\", option, washer.options),\n );\n\n const tags = splitList(record.mix_tags ?? \"\").map((tag) =>\n oneOf<MixTag>(row, \"mix_tags\", tag, mixTags),\n );\n\n // `ironing` decides whether there is a thermostat position at all, so the\n // two are checked together: a pile you never iron has nowhere to point the\n // dial, and a pile you do iron has to say where.\n const ironing = boolean(row, \"ironing\", record.ironing ?? \"\");\n const rawSetting = (record.iron_setting ?? \"\").trim();\n if (!ironing && rawSetting !== \"\") {\n throw new RowError(\n row,\n \"iron_setting\",\n `must be empty when ironing is no, found \"${rawSetting}\"`,\n );\n }\n const ironSetting = ironing\n ? oneOf(row, \"iron_setting\", rawSetting, ironSettingKeys(machine))\n : \"\";\n\n return {\n clothingType,\n detergent: record.detergent ?? \"\",\n fabricSoftener: boolean(row, \"fabric_softener\", record.fabric_softener ?? \"\"),\n temperature: oneOf(row, \"temperature\", record.temperature ?? \"\", washer.temperatures),\n spin: oneOf(row, \"spin\", record.spin ?? \"\", washer.spins),\n duration: record.duration ?? \"\",\n program: oneOf(row, \"program\", record.program ?? \"\", washer.programs),\n options,\n ironing,\n ironingNotes: record.ironing_notes ?? \"\",\n ironSetting,\n drying: record.drying ?? \"\",\n colourGroup: oneOf<ColourGroup>(row, \"colour_group\", record.colour_group ?? \"\", colourGroups),\n mixTags: tags,\n notes: record.notes ?? \"\",\n };\n });\n}\n\n/** The reverse of `instructionsFromRows` — what a JSON export writes out. */\nexport function rowsFromInstructions(instructions: Instruction[]): Row[] {\n return instructions.map((instruction) => ({\n clothing_type: instruction.clothingType,\n detergent: instruction.detergent,\n fabric_softener: instruction.fabricSoftener ? \"yes\" : \"no\",\n temperature: instruction.temperature,\n spin: instruction.spin,\n duration: instruction.duration,\n program: instruction.program,\n options: instruction.options.join(\"|\"),\n ironing: instruction.ironing ? \"yes\" : \"no\",\n ironing_notes: instruction.ironingNotes,\n iron_setting: instruction.ironSetting,\n drying: instruction.drying,\n colour_group: instruction.colourGroup,\n mix_tags: instruction.mixTags.join(\"|\"),\n notes: instruction.notes,\n }));\n}\n\n/**\n * The JSON interchange format for a chart — the same row shape\n * a chart's JSON Schema describes, so a file written here validates against\n * it exactly as a CSV does.\n */\nexport function chartToJson(instructions: Instruction[]): string {\n return `${JSON.stringify(rowsFromInstructions(instructions), null, 2)}\\n`;\n}\n\n/** The other half of `chartToJson`. Throws the same errors `parseInstructions` does. */\nexport function chartFromJson(source: string, machine: Machine): Instruction[] {\n let rows: unknown;\n try {\n rows = JSON.parse(source);\n } catch (error) {\n throw new Error(`not valid JSON: ${error instanceof Error ? error.message : error}`);\n }\n if (!Array.isArray(rows)) throw new Error(\"the chart must be a JSON array of rows\");\n return instructionsFromRows(rows as Record<string, string>[], machine);\n}\n","import { type Machine, parseMachine } from \"./machine\";\nimport { instructionsFromRows, rowsFromInstructions } from \"./rows\";\nimport type { Instruction } from \"./types\";\n\n/**\n * The washing machine and the chart that describes it, in one place. Both\n * halves are exactly what `parseMachine`/`instructionsFromRows` already\n * produce — this is a wrapper around the pair, not a third shape.\n */\nexport interface Config {\n machine: Machine;\n chart: Instruction[];\n}\n\n/**\n * Validates a `{ machine, chart }` object, checking the chart against the\n * machine it's embedded with — never a different one. Every failure names\n * which half is wrong, same as `parseMachine` and `instructionsFromRows` do\n * on their own.\n */\nexport function parseConfig(value: unknown): Config {\n if (typeof value !== \"object\" || value === null) {\n throw new Error(\"config: the file must contain an object\");\n }\n const raw = value as Record<string, unknown>;\n\n if (!(\"machine\" in raw)) throw new Error(\"config: machine is missing\");\n if (!(\"chart\" in raw)) throw new Error(\"config: chart is missing\");\n\n const machine = parseMachine(raw.machine);\n\n if (!Array.isArray(raw.chart)) throw new Error(\"config: chart must be an array of rows\");\n const chart = instructionsFromRows(raw.chart as Record<string, string>[], machine);\n\n return { machine, chart };\n}\n\n/** The reverse of `parseConfig` — what a JSON export writes out. */\nexport function configToJson(config: Config): string {\n const rows = { machine: config.machine, chart: rowsFromInstructions(config.chart) };\n return `${JSON.stringify(rows, null, 2)}\\n`;\n}\n\n/** The other half of `configToJson`. Throws the same errors `parseConfig` does. */\nexport function configFromJson(source: string): Config {\n let value: unknown;\n try {\n value = JSON.parse(source);\n } catch (error) {\n throw new Error(`config: not valid JSON: ${error instanceof Error ? error.message : error}`);\n }\n return parseConfig(value);\n}\n","import type { Instruction, ResolvedInstruction } from \"./types\";\n\n/** Why two piles cannot share a drum. `null` means they can. */\nexport type Blocker = \"solo\" | \"settings\" | \"colour\" | \"lint\";\n\nexport const blockerLegend: Record<Blocker, string> = {\n solo: \"Wash on its own\",\n settings: \"Different programme, temperature, spin or options\",\n colour: \"Colours would run into each other\",\n lint: \"One sheds lint onto the other\",\n};\n\n/** Short codes used in the compatibility matrix, so a cell stays one glyph. */\nexport const blockerCode: Record<Blocker, string> = {\n solo: \"S\",\n settings: \"P\",\n colour: \"C\",\n lint: \"L\",\n};\n\nfunction sameSettings(a: Instruction, b: Instruction): boolean {\n return (\n a.program === b.program &&\n a.temperature === b.temperature &&\n a.spin === b.spin &&\n a.options.length === b.options.length &&\n a.options.every((option) => b.options.includes(option))\n );\n}\n\nfunction compatibleColours(a: Instruction, b: Instruction): boolean {\n if (a.colourGroup === \"any\" || b.colourGroup === \"any\") return true;\n return a.colourGroup === b.colourGroup;\n}\n\n/**\n * Decides whether two piles can go in the drum together, and if not, why.\n *\n * The order matters: the reason reported is the one you would want to hear\n * first. \"Wash it alone\" beats \"the spin speed differs\", because changing the\n * spin speed would not help.\n */\nexport function mixBlocker(a: Instruction, b: Instruction): Blocker | null {\n if (a.mixTags.includes(\"solo\") || b.mixTags.includes(\"solo\")) return \"solo\";\n\n // Terry sheds lint over everything, so towels only ever go with towels.\n const shedders = [a, b].filter((item) => item.mixTags.includes(\"lint-shedder\"));\n if (shedders.length === 1) return \"lint\";\n\n if (!compatibleColours(a, b)) return \"colour\";\n if (!sameSettings(a, b)) return \"settings\";\n return null;\n}\n\nexport function canMix(a: Instruction, b: Instruction): boolean {\n return mixBlocker(a, b) === null;\n}\n\n/** Annotates each instruction with the other piles it may share a load with. */\nexport function resolve(instructions: Instruction[]): ResolvedInstruction[] {\n return instructions.map((instruction) => ({\n ...instruction,\n mixesWith: instructions\n .filter((other) => other !== instruction && canMix(instruction, other))\n .map((other) => other.clothingType),\n }));\n}\n\n/**\n * Groups piles that can all be washed together — every member compatible with\n * every other, not merely with the first one it met.\n */\nexport function loadGroups<T extends Instruction>(instructions: T[]): T[][] {\n const groups: T[][] = [];\n for (const instruction of instructions) {\n const home = groups.find((group) => group.every((member) => canMix(member, instruction)));\n if (home) home.push(instruction);\n else groups.push([instruction]);\n }\n return groups;\n}\n\nfunction groupBy<T>(items: T[], key: (item: T) => string): T[][] {\n const groups = new Map<string, T[]>();\n for (const item of items) {\n const existing = groups.get(key(item));\n if (existing) existing.push(item);\n else groups.set(key(item), [item]);\n }\n return [...groups.values()];\n}\n\n/**\n * Everything you physically set on the washing machine: the programme, what\n * the display reads, which buttons are lit, and whether softener goes in.\n *\n * Deliberately *not* the prose. Two piles can want the same five settings and\n * still want different detergent or a different drying rack, and those the card\n * lists per pile rather than splitting into two near-identical cards.\n */\nfunction washFingerprint(item: Instruction): string {\n return JSON.stringify([\n item.program,\n item.temperature,\n item.spin,\n [...item.options].sort(),\n item.fabricSoftener,\n ]);\n}\n\n/**\n * Piles you set the machine and the iron up for identically. Every dial\n * drawing on their cards would be the same drawing, so they get one card\n * listing all the names.\n */\nexport function cardGroups<T extends Instruction>(instructions: T[]): T[][] {\n return groupBy(instructions, (item) =>\n JSON.stringify([washFingerprint(item), item.ironing, item.ironSetting]),\n );\n}\n\n/**\n * The same, for a sheet with no iron on it. Dark, Black Socks and Denim each\n * need their own card on the full chart only because they want three different\n * thermostat positions; standing at the machine they are one wash.\n */\nexport function washGroups<T extends Instruction>(instructions: T[]): T[][] {\n return groupBy(instructions, washFingerprint);\n}\n\n/**\n * Piles by where the iron's thermostat points, coolest first.\n *\n * An ironing sheet is read the other way round from a washing one. You do not\n * fetch a pile and look up its setting — you set the iron once and work through\n * everything that goes at that heat, so the thermostat position is the heading\n * and the piles are the list under it. `order` is `ironSettingKeys`; a pile you\n * never iron has no position in it and sorts last, which is right — it is the\n * pile you never pick up.\n */\nexport function ironGroups<T extends Instruction>(\n instructions: T[],\n order: readonly string[],\n): T[][] {\n const rank = (setting: string) => {\n const at = order.indexOf(setting);\n return at < 0 ? order.length : at;\n };\n return groupBy(instructions, (item) => item.ironSetting).sort(\n (a, b) => rank((a[0] as T).ironSetting) - rank((b[0] as T).ironSetting),\n );\n}\n"],"mappings":"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"}