@washy-washy/core 0.0.0 → 1.0.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/dist/browser.d.mts +2 -0
- package/dist/browser.mjs +2 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +18 -0
- package/dist/index.mjs.map +1 -0
- package/dist/mixing-CazkIyLl.mjs +378 -0
- package/dist/mixing-CazkIyLl.mjs.map +1 -0
- package/dist/rows-lRFNdyJh.d.mts +198 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { A as Washer, C as colourGroups, D as Iron, E as variants, M as ironSetting, N as ironSettingKeys, O as IronSetting, P as parseMachine, S as Variant, T as mixTags, _ as washGroups, a as chartToJson, b as MixTag, c as Blocker, d as canMix, f as cardGroups, g as resolve, h as mixBlocker, i as chartFromJson, j as formatTemperature, k as Machine, 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 ColourGroup, w as durationsOf, x as ResolvedInstruction, y as Instruction } from "./rows-lRFNdyJh.mjs";
|
|
2
|
+
export { Blocker, COLUMNS, ColourGroup, Instruction, Iron, IronSetting, Machine, MixTag, ResolvedInstruction, Row, RowError, Variant, Washer, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
|
package/dist/browser.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { C as parseMachine, S as ironSettingKeys, _ as durationsOf, a as ironGroups, b as formatTemperature, c as resolve, d as RowError, f as chartFromJson, g as colourGroups, h as rowsFromInstructions, i as cardGroups, l as washGroups, m as instructionsFromRows, n as blockerLegend, o as loadGroups, p as chartToJson, r as canMix, s as mixBlocker, t as blockerCode, u as COLUMNS, v as mixTags, x as ironSetting, y as variants } from "./mixing-CazkIyLl.mjs";
|
|
2
|
+
export { COLUMNS, RowError, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { A as Washer, C as colourGroups, D as Iron, E as variants, M as ironSetting, N as ironSettingKeys, O as IronSetting, P as parseMachine, S as Variant, T as mixTags, _ as washGroups, a as chartToJson, b as MixTag, c as Blocker, d as canMix, f as cardGroups, g as resolve, h as mixBlocker, i as chartFromJson, j as formatTemperature, k as Machine, 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 ColourGroup, w as durationsOf, x as ResolvedInstruction, y as Instruction } from "./rows-lRFNdyJh.mjs";
|
|
2
|
+
//#region src/csv.d.ts
|
|
3
|
+
/** Parses the instruction CSV. See `instructionsFromRows` for the row rules. */
|
|
4
|
+
declare function parseInstructions(source: string, machine: Machine): Instruction[];
|
|
5
|
+
//#endregion
|
|
6
|
+
export { Blocker, COLUMNS, ColourGroup, Instruction, Iron, IronSetting, Machine, MixTag, ResolvedInstruction, Row, RowError, Variant, Washer, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseInstructions, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
|
|
7
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { C as parseMachine, S as ironSettingKeys, _ as durationsOf, a as ironGroups, b as formatTemperature, c as resolve, d as RowError, f as chartFromJson, g as colourGroups, h as rowsFromInstructions, i as cardGroups, l as washGroups, m as instructionsFromRows, n as blockerLegend, o as loadGroups, p as chartToJson, r as canMix, s as mixBlocker, t as blockerCode, u as COLUMNS, v as mixTags, x as ironSetting, y as variants } from "./mixing-CazkIyLl.mjs";
|
|
2
|
+
import { parse } from "csv-parse/sync";
|
|
3
|
+
//#region src/csv.ts
|
|
4
|
+
/** Parses the instruction CSV. See `instructionsFromRows` for the row rules. */
|
|
5
|
+
function parseInstructions(source, machine) {
|
|
6
|
+
const records = parse(source, {
|
|
7
|
+
columns: true,
|
|
8
|
+
skip_empty_lines: true,
|
|
9
|
+
trim: true,
|
|
10
|
+
bom: true
|
|
11
|
+
});
|
|
12
|
+
if (records.length === 0) throw new Error("the CSV has a header but no rows");
|
|
13
|
+
return instructionsFromRows(records, machine);
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { COLUMNS, RowError, blockerCode, blockerLegend, canMix, cardGroups, chartFromJson, chartToJson, colourGroups, durationsOf, formatTemperature, instructionsFromRows, ironGroups, ironSetting, ironSettingKeys, loadGroups, mixBlocker, mixTags, parseInstructions, parseMachine, resolve, rowsFromInstructions, variants, washGroups };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
//#region src/machine.ts
|
|
2
|
+
function fail(what) {
|
|
3
|
+
throw new Error(`machine: ${what}`);
|
|
4
|
+
}
|
|
5
|
+
function stringList(value, field, minimum) {
|
|
6
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry === "")) fail(`${field} must be a list of non-empty strings`);
|
|
7
|
+
const list = value;
|
|
8
|
+
if (list.length < minimum) fail(`${field} needs at least ${minimum} entries, found ${list.length}`);
|
|
9
|
+
if (new Set(list).size !== list.length) fail(`${field} repeats a value`);
|
|
10
|
+
return list;
|
|
11
|
+
}
|
|
12
|
+
function text(value, field, fallback) {
|
|
13
|
+
if (typeof value === "string" && value !== "") return value;
|
|
14
|
+
if (fallback !== void 0) return fallback;
|
|
15
|
+
return fail(`${field} must be a non-empty string`);
|
|
16
|
+
}
|
|
17
|
+
function parseIronSetting(value, index) {
|
|
18
|
+
if (typeof value !== "object" || value === null) fail(`iron.settings[${index}] must be an object`);
|
|
19
|
+
const raw = value;
|
|
20
|
+
return {
|
|
21
|
+
key: text(raw.key, `iron.settings[${index}].key`),
|
|
22
|
+
dots: typeof raw.dots === "string" ? raw.dots : "",
|
|
23
|
+
label: text(raw.label, `iron.settings[${index}].label`),
|
|
24
|
+
detail: text(raw.detail, `iron.settings[${index}].detail`, ""),
|
|
25
|
+
steam: raw.steam === true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Checks a parsed machine file and hands back something the renderer can use.
|
|
30
|
+
* Every failure names the field, because the fix is always an edit to that
|
|
31
|
+
* line of JSON.
|
|
32
|
+
*/
|
|
33
|
+
function parseMachine(value) {
|
|
34
|
+
if (typeof value !== "object" || value === null) fail("the file must contain an object");
|
|
35
|
+
const raw = value;
|
|
36
|
+
if (typeof raw.washer !== "object" || raw.washer === null) fail("washer is missing");
|
|
37
|
+
if (typeof raw.iron !== "object" || raw.iron === null) fail("iron is missing");
|
|
38
|
+
const washerRaw = raw.washer;
|
|
39
|
+
const ironRaw = raw.iron;
|
|
40
|
+
const settings = Array.isArray(ironRaw.settings) ? ironRaw.settings : fail("iron.settings is missing");
|
|
41
|
+
if (settings.length < 2) fail("iron.settings needs at least 2 positions to draw a ring");
|
|
42
|
+
const keys = settings.map((setting, index) => parseIronSetting(setting, index));
|
|
43
|
+
if (new Set(keys.map((setting) => setting.key)).size !== keys.length) fail("iron.settings repeats a key");
|
|
44
|
+
return {
|
|
45
|
+
washer: {
|
|
46
|
+
name: text(washerRaw.name, "washer.name"),
|
|
47
|
+
capacity: text(washerRaw.capacity, "washer.capacity", ""),
|
|
48
|
+
programs: stringList(washerRaw.programs, "washer.programs", 2),
|
|
49
|
+
temperatures: stringList(washerRaw.temperatures, "washer.temperatures", 1),
|
|
50
|
+
spins: stringList(washerRaw.spins, "washer.spins", 1),
|
|
51
|
+
options: stringList(washerRaw.options ?? [], "washer.options", 0)
|
|
52
|
+
},
|
|
53
|
+
iron: {
|
|
54
|
+
name: text(ironRaw.name, "iron.name"),
|
|
55
|
+
settings: keys
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function ironSetting(machine, key) {
|
|
60
|
+
return machine.iron.settings.find((setting) => setting.key === key);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The keys a row may write in `iron_setting`, coolest first.
|
|
64
|
+
*
|
|
65
|
+
* There is no key for "do not iron". That is the `ironing` column's job, and
|
|
66
|
+
* two places to say the same thing is two places to disagree.
|
|
67
|
+
*/
|
|
68
|
+
function ironSettingKeys(machine) {
|
|
69
|
+
return machine.iron.settings.map((setting) => setting.key);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `40` becomes `40°`; `cold` stays `cold`. The machine file is free to use
|
|
73
|
+
* whatever word its display shows for the cold wash, so the degree sign is
|
|
74
|
+
* decided by whether the value is a number rather than by a list of exceptions.
|
|
75
|
+
*/
|
|
76
|
+
function formatTemperature(value) {
|
|
77
|
+
return /^\d+$/.test(value) ? `${value}°` : value;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/types.ts
|
|
81
|
+
/** Which pile a garment belongs to on colour grounds. `any` mixes with all. */
|
|
82
|
+
const colourGroups = [
|
|
83
|
+
"white",
|
|
84
|
+
"colour",
|
|
85
|
+
"dark",
|
|
86
|
+
"sport",
|
|
87
|
+
"any"
|
|
88
|
+
];
|
|
89
|
+
/**
|
|
90
|
+
* Reasons two piles might not belong together even when the machine settings
|
|
91
|
+
* agree. See `canMix` in `mixing.ts` for how each one is applied.
|
|
92
|
+
*/
|
|
93
|
+
const mixTags = [
|
|
94
|
+
"lint-shedder",
|
|
95
|
+
"lint-magnet",
|
|
96
|
+
"dye-bleeder",
|
|
97
|
+
"solo"
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* How long a card or a load is going to tie the machine up.
|
|
101
|
+
*
|
|
102
|
+
* Usually one figure. Piles sharing a load agree on every setting, so they
|
|
103
|
+
* ought to agree on this too — but the chart is data somebody types, and
|
|
104
|
+
* printing both is more honest than picking the first one and hoping.
|
|
105
|
+
*/
|
|
106
|
+
function durationsOf(group) {
|
|
107
|
+
return [...new Set(group.map((item) => item.duration).filter((value) => value !== ""))].join(" / ");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Which sheet to draw.
|
|
111
|
+
*
|
|
112
|
+
* `full` is the whole chart. The other two are the same chart cut where the
|
|
113
|
+
* work is: washing happens in front of the machine on a Sunday morning, ironing
|
|
114
|
+
* happens at a board on a Wednesday evening, and neither job wants to read past
|
|
115
|
+
* the other's advice to find its own.
|
|
116
|
+
*/
|
|
117
|
+
const variants = [
|
|
118
|
+
"full",
|
|
119
|
+
"wash",
|
|
120
|
+
"iron"
|
|
121
|
+
];
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/rows.ts
|
|
124
|
+
const COLUMNS = [
|
|
125
|
+
"clothing_type",
|
|
126
|
+
"detergent",
|
|
127
|
+
"fabric_softener",
|
|
128
|
+
"temperature",
|
|
129
|
+
"spin",
|
|
130
|
+
"duration",
|
|
131
|
+
"program",
|
|
132
|
+
"options",
|
|
133
|
+
"ironing",
|
|
134
|
+
"ironing_notes",
|
|
135
|
+
"iron_setting",
|
|
136
|
+
"drying",
|
|
137
|
+
"colour_group",
|
|
138
|
+
"mix_tags",
|
|
139
|
+
"notes"
|
|
140
|
+
];
|
|
141
|
+
var RowError = class extends Error {
|
|
142
|
+
constructor(row, column, message) {
|
|
143
|
+
super(`row ${row}, column "${column}": ${message}`);
|
|
144
|
+
this.name = "RowError";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
/** `a|b|c` -> `["a", "b", "c"]`, tolerating stray whitespace and empties. */
|
|
148
|
+
function splitList(value) {
|
|
149
|
+
return value.split("|").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
150
|
+
}
|
|
151
|
+
function oneOf(row, column, value, allowed) {
|
|
152
|
+
if (!allowed.includes(value)) throw new RowError(row, column, `"${value}" is not one of ${allowed.join(", ")}`);
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function boolean(row, column, value) {
|
|
156
|
+
const normalised = value.trim().toLowerCase();
|
|
157
|
+
if ([
|
|
158
|
+
"yes",
|
|
159
|
+
"y",
|
|
160
|
+
"true",
|
|
161
|
+
"1"
|
|
162
|
+
].includes(normalised)) return true;
|
|
163
|
+
if ([
|
|
164
|
+
"no",
|
|
165
|
+
"n",
|
|
166
|
+
"false",
|
|
167
|
+
"0"
|
|
168
|
+
].includes(normalised)) return false;
|
|
169
|
+
throw new RowError(row, column, `"${value}" is not a yes/no value`);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Checks rows shaped like the chart's schema — the same shape a CSV parses
|
|
173
|
+
* into, or a JSON file round-tripped through `rowsFromInstructions` — against
|
|
174
|
+
* what the appliances in the machine file can actually be set to. A typo in a
|
|
175
|
+
* programme name fails here rather than producing a PDF that tells you to turn
|
|
176
|
+
* the dial to a position that does not exist — and so does a chart written for
|
|
177
|
+
* a different machine than the one you passed.
|
|
178
|
+
*/
|
|
179
|
+
function instructionsFromRows(rows, machine) {
|
|
180
|
+
const { washer } = machine;
|
|
181
|
+
if (rows.length === 0) throw new Error("the chart has no rows");
|
|
182
|
+
const header = Object.keys(rows[0]);
|
|
183
|
+
const missing = COLUMNS.filter((column) => !header.includes(column));
|
|
184
|
+
if (missing.length > 0) throw new Error(`the chart is missing column(s): ${missing.join(", ")}`);
|
|
185
|
+
return rows.map((record, index) => {
|
|
186
|
+
const row = index + 2;
|
|
187
|
+
const clothingType = (record.clothing_type ?? "").trim();
|
|
188
|
+
if (clothingType === "") throw new RowError(row, "clothing_type", "must not be empty");
|
|
189
|
+
const options = splitList(record.options ?? "").map((option) => oneOf(row, "options", option, washer.options));
|
|
190
|
+
const tags = splitList(record.mix_tags ?? "").map((tag) => oneOf(row, "mix_tags", tag, mixTags));
|
|
191
|
+
const ironing = boolean(row, "ironing", record.ironing ?? "");
|
|
192
|
+
const rawSetting = (record.iron_setting ?? "").trim();
|
|
193
|
+
if (!ironing && rawSetting !== "") throw new RowError(row, "iron_setting", `must be empty when ironing is no, found "${rawSetting}"`);
|
|
194
|
+
const ironSetting = ironing ? oneOf(row, "iron_setting", rawSetting, ironSettingKeys(machine)) : "";
|
|
195
|
+
return {
|
|
196
|
+
clothingType,
|
|
197
|
+
detergent: record.detergent ?? "",
|
|
198
|
+
fabricSoftener: boolean(row, "fabric_softener", record.fabric_softener ?? ""),
|
|
199
|
+
temperature: oneOf(row, "temperature", record.temperature ?? "", washer.temperatures),
|
|
200
|
+
spin: oneOf(row, "spin", record.spin ?? "", washer.spins),
|
|
201
|
+
duration: record.duration ?? "",
|
|
202
|
+
program: oneOf(row, "program", record.program ?? "", washer.programs),
|
|
203
|
+
options,
|
|
204
|
+
ironing,
|
|
205
|
+
ironingNotes: record.ironing_notes ?? "",
|
|
206
|
+
ironSetting,
|
|
207
|
+
drying: record.drying ?? "",
|
|
208
|
+
colourGroup: oneOf(row, "colour_group", record.colour_group ?? "", colourGroups),
|
|
209
|
+
mixTags: tags,
|
|
210
|
+
notes: record.notes ?? ""
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/** The reverse of `instructionsFromRows` — what a JSON export writes out. */
|
|
215
|
+
function rowsFromInstructions(instructions) {
|
|
216
|
+
return instructions.map((instruction) => ({
|
|
217
|
+
clothing_type: instruction.clothingType,
|
|
218
|
+
detergent: instruction.detergent,
|
|
219
|
+
fabric_softener: instruction.fabricSoftener ? "yes" : "no",
|
|
220
|
+
temperature: instruction.temperature,
|
|
221
|
+
spin: instruction.spin,
|
|
222
|
+
duration: instruction.duration,
|
|
223
|
+
program: instruction.program,
|
|
224
|
+
options: instruction.options.join("|"),
|
|
225
|
+
ironing: instruction.ironing ? "yes" : "no",
|
|
226
|
+
ironing_notes: instruction.ironingNotes,
|
|
227
|
+
iron_setting: instruction.ironSetting,
|
|
228
|
+
drying: instruction.drying,
|
|
229
|
+
colour_group: instruction.colourGroup,
|
|
230
|
+
mix_tags: instruction.mixTags.join("|"),
|
|
231
|
+
notes: instruction.notes
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The JSON interchange format for a chart — the same row shape
|
|
236
|
+
* a chart's JSON Schema describes, so a file written here validates against
|
|
237
|
+
* it exactly as a CSV does.
|
|
238
|
+
*/
|
|
239
|
+
function chartToJson(instructions) {
|
|
240
|
+
return `${JSON.stringify(rowsFromInstructions(instructions), null, 2)}\n`;
|
|
241
|
+
}
|
|
242
|
+
/** The other half of `chartToJson`. Throws the same errors `parseInstructions` does. */
|
|
243
|
+
function chartFromJson(source, machine) {
|
|
244
|
+
let rows;
|
|
245
|
+
try {
|
|
246
|
+
rows = JSON.parse(source);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
throw new Error(`not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
249
|
+
}
|
|
250
|
+
if (!Array.isArray(rows)) throw new Error("the chart must be a JSON array of rows");
|
|
251
|
+
return instructionsFromRows(rows, machine);
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/mixing.ts
|
|
255
|
+
const blockerLegend = {
|
|
256
|
+
solo: "Wash on its own",
|
|
257
|
+
settings: "Different programme, temperature, spin or options",
|
|
258
|
+
colour: "Colours would run into each other",
|
|
259
|
+
lint: "One sheds lint onto the other"
|
|
260
|
+
};
|
|
261
|
+
/** Short codes used in the compatibility matrix, so a cell stays one glyph. */
|
|
262
|
+
const blockerCode = {
|
|
263
|
+
solo: "S",
|
|
264
|
+
settings: "P",
|
|
265
|
+
colour: "C",
|
|
266
|
+
lint: "L"
|
|
267
|
+
};
|
|
268
|
+
function sameSettings(a, b) {
|
|
269
|
+
return a.program === b.program && a.temperature === b.temperature && a.spin === b.spin && a.options.length === b.options.length && a.options.every((option) => b.options.includes(option));
|
|
270
|
+
}
|
|
271
|
+
function compatibleColours(a, b) {
|
|
272
|
+
if (a.colourGroup === "any" || b.colourGroup === "any") return true;
|
|
273
|
+
return a.colourGroup === b.colourGroup;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Decides whether two piles can go in the drum together, and if not, why.
|
|
277
|
+
*
|
|
278
|
+
* The order matters: the reason reported is the one you would want to hear
|
|
279
|
+
* first. "Wash it alone" beats "the spin speed differs", because changing the
|
|
280
|
+
* spin speed would not help.
|
|
281
|
+
*/
|
|
282
|
+
function mixBlocker(a, b) {
|
|
283
|
+
if (a.mixTags.includes("solo") || b.mixTags.includes("solo")) return "solo";
|
|
284
|
+
if ([a, b].filter((item) => item.mixTags.includes("lint-shedder")).length === 1) return "lint";
|
|
285
|
+
if (!compatibleColours(a, b)) return "colour";
|
|
286
|
+
if (!sameSettings(a, b)) return "settings";
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
function canMix(a, b) {
|
|
290
|
+
return mixBlocker(a, b) === null;
|
|
291
|
+
}
|
|
292
|
+
/** Annotates each instruction with the other piles it may share a load with. */
|
|
293
|
+
function resolve(instructions) {
|
|
294
|
+
return instructions.map((instruction) => ({
|
|
295
|
+
...instruction,
|
|
296
|
+
mixesWith: instructions.filter((other) => other !== instruction && canMix(instruction, other)).map((other) => other.clothingType)
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Groups piles that can all be washed together — every member compatible with
|
|
301
|
+
* every other, not merely with the first one it met.
|
|
302
|
+
*/
|
|
303
|
+
function loadGroups(instructions) {
|
|
304
|
+
const groups = [];
|
|
305
|
+
for (const instruction of instructions) {
|
|
306
|
+
const home = groups.find((group) => group.every((member) => canMix(member, instruction)));
|
|
307
|
+
if (home) home.push(instruction);
|
|
308
|
+
else groups.push([instruction]);
|
|
309
|
+
}
|
|
310
|
+
return groups;
|
|
311
|
+
}
|
|
312
|
+
function groupBy(items, key) {
|
|
313
|
+
const groups = /* @__PURE__ */ new Map();
|
|
314
|
+
for (const item of items) {
|
|
315
|
+
const existing = groups.get(key(item));
|
|
316
|
+
if (existing) existing.push(item);
|
|
317
|
+
else groups.set(key(item), [item]);
|
|
318
|
+
}
|
|
319
|
+
return [...groups.values()];
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Everything you physically set on the washing machine: the programme, what
|
|
323
|
+
* the display reads, which buttons are lit, and whether softener goes in.
|
|
324
|
+
*
|
|
325
|
+
* Deliberately *not* the prose. Two piles can want the same five settings and
|
|
326
|
+
* still want different detergent or a different drying rack, and those the card
|
|
327
|
+
* lists per pile rather than splitting into two near-identical cards.
|
|
328
|
+
*/
|
|
329
|
+
function washFingerprint(item) {
|
|
330
|
+
return JSON.stringify([
|
|
331
|
+
item.program,
|
|
332
|
+
item.temperature,
|
|
333
|
+
item.spin,
|
|
334
|
+
[...item.options].sort(),
|
|
335
|
+
item.fabricSoftener
|
|
336
|
+
]);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Piles you set the machine and the iron up for identically. Every dial
|
|
340
|
+
* drawing on their cards would be the same drawing, so they get one card
|
|
341
|
+
* listing all the names.
|
|
342
|
+
*/
|
|
343
|
+
function cardGroups(instructions) {
|
|
344
|
+
return groupBy(instructions, (item) => JSON.stringify([
|
|
345
|
+
washFingerprint(item),
|
|
346
|
+
item.ironing,
|
|
347
|
+
item.ironSetting
|
|
348
|
+
]));
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The same, for a sheet with no iron on it. Dark, Black Socks and Denim each
|
|
352
|
+
* need their own card on the full chart only because they want three different
|
|
353
|
+
* thermostat positions; standing at the machine they are one wash.
|
|
354
|
+
*/
|
|
355
|
+
function washGroups(instructions) {
|
|
356
|
+
return groupBy(instructions, washFingerprint);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Piles by where the iron's thermostat points, coolest first.
|
|
360
|
+
*
|
|
361
|
+
* An ironing sheet is read the other way round from a washing one. You do not
|
|
362
|
+
* fetch a pile and look up its setting — you set the iron once and work through
|
|
363
|
+
* everything that goes at that heat, so the thermostat position is the heading
|
|
364
|
+
* and the piles are the list under it. `order` is `ironSettingKeys`; a pile you
|
|
365
|
+
* never iron has no position in it and sorts last, which is right — it is the
|
|
366
|
+
* pile you never pick up.
|
|
367
|
+
*/
|
|
368
|
+
function ironGroups(instructions, order) {
|
|
369
|
+
const rank = (setting) => {
|
|
370
|
+
const at = order.indexOf(setting);
|
|
371
|
+
return at < 0 ? order.length : at;
|
|
372
|
+
};
|
|
373
|
+
return groupBy(instructions, (item) => item.ironSetting).sort((a, b) => rank(a[0].ironSetting) - rank(b[0].ironSetting));
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
export { parseMachine as C, ironSettingKeys as S, durationsOf as _, ironGroups as a, formatTemperature as b, resolve as c, RowError as d, chartFromJson as f, colourGroups as g, rowsFromInstructions as h, cardGroups as i, washGroups as l, instructionsFromRows as m, blockerLegend as n, loadGroups as o, chartToJson as p, canMix as r, mixBlocker as s, blockerCode as t, COLUMNS as u, mixTags as v, ironSetting as x, variants as y };
|
|
377
|
+
|
|
378
|
+
//# sourceMappingURL=mixing-CazkIyLl.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mixing-CazkIyLl.mjs","names":[],"sources":["../src/machine.ts","../src/types.ts","../src/rows.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 { 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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"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
//#region src/machine.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The appliances a chart is drawn for, as data rather than code.
|
|
4
|
+
*
|
|
5
|
+
* Everything the PDFs render — dial angles, which chips exist, what a valid CSV
|
|
6
|
+
* value is — comes from a machine file, so pointing this at a different washing
|
|
7
|
+
* machine is editing JSON rather than editing TypeScript. The labels in that
|
|
8
|
+
* file are copied off the fascia in whatever language it is printed in, and
|
|
9
|
+
* nothing here ever translates them: a chart that says "Cottons" when the dial
|
|
10
|
+
* says "Katoen" is worse than no chart, because you have to translate it back
|
|
11
|
+
* while standing in front of the machine.
|
|
12
|
+
*/
|
|
13
|
+
interface Washer {
|
|
14
|
+
name: string;
|
|
15
|
+
capacity: string;
|
|
16
|
+
/** Dial labels in physical order from twelve o'clock, clockwise. Index is the angle. */
|
|
17
|
+
programs: string[];
|
|
18
|
+
temperatures: string[];
|
|
19
|
+
spins: string[];
|
|
20
|
+
options: string[];
|
|
21
|
+
}
|
|
22
|
+
interface IronSetting {
|
|
23
|
+
key: string;
|
|
24
|
+
dots: string;
|
|
25
|
+
label: string;
|
|
26
|
+
detail: string;
|
|
27
|
+
steam: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface Iron {
|
|
30
|
+
name: string;
|
|
31
|
+
/** Thermostat positions in order, coolest first. */
|
|
32
|
+
settings: IronSetting[];
|
|
33
|
+
}
|
|
34
|
+
interface Machine {
|
|
35
|
+
washer: Washer;
|
|
36
|
+
iron: Iron;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Checks a parsed machine file and hands back something the renderer can use.
|
|
40
|
+
* Every failure names the field, because the fix is always an edit to that
|
|
41
|
+
* line of JSON.
|
|
42
|
+
*/
|
|
43
|
+
declare function parseMachine(value: unknown): Machine;
|
|
44
|
+
declare function ironSetting(machine: Machine, key: string): IronSetting | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The keys a row may write in `iron_setting`, coolest first.
|
|
47
|
+
*
|
|
48
|
+
* There is no key for "do not iron". That is the `ironing` column's job, and
|
|
49
|
+
* two places to say the same thing is two places to disagree.
|
|
50
|
+
*/
|
|
51
|
+
declare function ironSettingKeys(machine: Machine): string[];
|
|
52
|
+
/**
|
|
53
|
+
* `40` becomes `40°`; `cold` stays `cold`. The machine file is free to use
|
|
54
|
+
* whatever word its display shows for the cold wash, so the degree sign is
|
|
55
|
+
* decided by whether the value is a number rather than by a list of exceptions.
|
|
56
|
+
*/
|
|
57
|
+
declare function formatTemperature(value: string): string;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/types.d.ts
|
|
60
|
+
/** Which pile a garment belongs to on colour grounds. `any` mixes with all. */
|
|
61
|
+
declare const colourGroups: readonly ["white", "colour", "dark", "sport", "any"];
|
|
62
|
+
type ColourGroup = (typeof colourGroups)[number];
|
|
63
|
+
/**
|
|
64
|
+
* Reasons two piles might not belong together even when the machine settings
|
|
65
|
+
* agree. See `canMix` in `mixing.ts` for how each one is applied.
|
|
66
|
+
*/
|
|
67
|
+
declare const mixTags: readonly ["lint-shedder", "lint-magnet", "dye-bleeder", "solo"];
|
|
68
|
+
type MixTag = (typeof mixTags)[number];
|
|
69
|
+
/**
|
|
70
|
+
* One CSV row: everything needed to wash, iron and dry one pile of laundry.
|
|
71
|
+
*
|
|
72
|
+
* The machine-facing fields are plain strings rather than unions, because what
|
|
73
|
+
* counts as valid depends on the machine file you point this at. `csv.ts`
|
|
74
|
+
* checks each of them against that machine as it parses, so nothing downstream
|
|
75
|
+
* has to wonder whether a programme name exists.
|
|
76
|
+
*/
|
|
77
|
+
interface Instruction {
|
|
78
|
+
clothingType: string;
|
|
79
|
+
detergent: string;
|
|
80
|
+
fabricSoftener: boolean;
|
|
81
|
+
temperature: string;
|
|
82
|
+
spin: string;
|
|
83
|
+
duration: string;
|
|
84
|
+
program: string;
|
|
85
|
+
options: string[];
|
|
86
|
+
/** Whether this pile gets ironed at all. */
|
|
87
|
+
ironing: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* How to iron it, or why you don't. Free prose and often empty, because most
|
|
90
|
+
* of what there is to say about a pile nobody irons is already said by
|
|
91
|
+
* `ironing` being false.
|
|
92
|
+
*/
|
|
93
|
+
ironingNotes: string;
|
|
94
|
+
/** Thermostat position. Empty when `ironing` is false. */
|
|
95
|
+
ironSetting: string;
|
|
96
|
+
drying: string;
|
|
97
|
+
colourGroup: ColourGroup;
|
|
98
|
+
mixTags: MixTag[];
|
|
99
|
+
notes: string;
|
|
100
|
+
}
|
|
101
|
+
/** An instruction plus the other piles it may share a drum with. */
|
|
102
|
+
interface ResolvedInstruction extends Instruction {
|
|
103
|
+
mixesWith: string[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* How long a card or a load is going to tie the machine up.
|
|
107
|
+
*
|
|
108
|
+
* Usually one figure. Piles sharing a load agree on every setting, so they
|
|
109
|
+
* ought to agree on this too — but the chart is data somebody types, and
|
|
110
|
+
* printing both is more honest than picking the first one and hoping.
|
|
111
|
+
*/
|
|
112
|
+
declare function durationsOf(group: Instruction[]): string;
|
|
113
|
+
/**
|
|
114
|
+
* Which sheet to draw.
|
|
115
|
+
*
|
|
116
|
+
* `full` is the whole chart. The other two are the same chart cut where the
|
|
117
|
+
* work is: washing happens in front of the machine on a Sunday morning, ironing
|
|
118
|
+
* happens at a board on a Wednesday evening, and neither job wants to read past
|
|
119
|
+
* the other's advice to find its own.
|
|
120
|
+
*/
|
|
121
|
+
declare const variants: readonly ["full", "wash", "iron"];
|
|
122
|
+
type Variant = (typeof variants)[number];
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/mixing.d.ts
|
|
125
|
+
/** Why two piles cannot share a drum. `null` means they can. */
|
|
126
|
+
type Blocker = "solo" | "settings" | "colour" | "lint";
|
|
127
|
+
declare const blockerLegend: Record<Blocker, string>;
|
|
128
|
+
/** Short codes used in the compatibility matrix, so a cell stays one glyph. */
|
|
129
|
+
declare const blockerCode: Record<Blocker, string>;
|
|
130
|
+
/**
|
|
131
|
+
* Decides whether two piles can go in the drum together, and if not, why.
|
|
132
|
+
*
|
|
133
|
+
* The order matters: the reason reported is the one you would want to hear
|
|
134
|
+
* first. "Wash it alone" beats "the spin speed differs", because changing the
|
|
135
|
+
* spin speed would not help.
|
|
136
|
+
*/
|
|
137
|
+
declare function mixBlocker(a: Instruction, b: Instruction): Blocker | null;
|
|
138
|
+
declare function canMix(a: Instruction, b: Instruction): boolean;
|
|
139
|
+
/** Annotates each instruction with the other piles it may share a load with. */
|
|
140
|
+
declare function resolve(instructions: Instruction[]): ResolvedInstruction[];
|
|
141
|
+
/**
|
|
142
|
+
* Groups piles that can all be washed together — every member compatible with
|
|
143
|
+
* every other, not merely with the first one it met.
|
|
144
|
+
*/
|
|
145
|
+
declare function loadGroups<T extends Instruction>(instructions: T[]): T[][];
|
|
146
|
+
/**
|
|
147
|
+
* Piles you set the machine and the iron up for identically. Every dial
|
|
148
|
+
* drawing on their cards would be the same drawing, so they get one card
|
|
149
|
+
* listing all the names.
|
|
150
|
+
*/
|
|
151
|
+
declare function cardGroups<T extends Instruction>(instructions: T[]): T[][];
|
|
152
|
+
/**
|
|
153
|
+
* The same, for a sheet with no iron on it. Dark, Black Socks and Denim each
|
|
154
|
+
* need their own card on the full chart only because they want three different
|
|
155
|
+
* thermostat positions; standing at the machine they are one wash.
|
|
156
|
+
*/
|
|
157
|
+
declare function washGroups<T extends Instruction>(instructions: T[]): T[][];
|
|
158
|
+
/**
|
|
159
|
+
* Piles by where the iron's thermostat points, coolest first.
|
|
160
|
+
*
|
|
161
|
+
* An ironing sheet is read the other way round from a washing one. You do not
|
|
162
|
+
* fetch a pile and look up its setting — you set the iron once and work through
|
|
163
|
+
* everything that goes at that heat, so the thermostat position is the heading
|
|
164
|
+
* and the piles are the list under it. `order` is `ironSettingKeys`; a pile you
|
|
165
|
+
* never iron has no position in it and sorts last, which is right — it is the
|
|
166
|
+
* pile you never pick up.
|
|
167
|
+
*/
|
|
168
|
+
declare function ironGroups<T extends Instruction>(instructions: T[], order: readonly string[]): T[][];
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/rows.d.ts
|
|
171
|
+
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"];
|
|
172
|
+
/** One row, keyed the same way as a CSV column and the chart's JSON Schema. */
|
|
173
|
+
type Row = Record<(typeof COLUMNS)[number], string>;
|
|
174
|
+
declare class RowError extends Error {
|
|
175
|
+
constructor(row: number, column: string, message: string);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Checks rows shaped like the chart's schema — the same shape a CSV parses
|
|
179
|
+
* into, or a JSON file round-tripped through `rowsFromInstructions` — against
|
|
180
|
+
* what the appliances in the machine file can actually be set to. A typo in a
|
|
181
|
+
* programme name fails here rather than producing a PDF that tells you to turn
|
|
182
|
+
* the dial to a position that does not exist — and so does a chart written for
|
|
183
|
+
* a different machine than the one you passed.
|
|
184
|
+
*/
|
|
185
|
+
declare function instructionsFromRows(rows: Record<string, string>[], machine: Machine): Instruction[];
|
|
186
|
+
/** The reverse of `instructionsFromRows` — what a JSON export writes out. */
|
|
187
|
+
declare function rowsFromInstructions(instructions: Instruction[]): Row[];
|
|
188
|
+
/**
|
|
189
|
+
* The JSON interchange format for a chart — the same row shape
|
|
190
|
+
* a chart's JSON Schema describes, so a file written here validates against
|
|
191
|
+
* it exactly as a CSV does.
|
|
192
|
+
*/
|
|
193
|
+
declare function chartToJson(instructions: Instruction[]): string;
|
|
194
|
+
/** The other half of `chartToJson`. Throws the same errors `parseInstructions` does. */
|
|
195
|
+
declare function chartFromJson(source: string, machine: Machine): Instruction[];
|
|
196
|
+
//#endregion
|
|
197
|
+
export { Washer as A, colourGroups as C, Iron as D, variants as E, ironSetting as M, ironSettingKeys as N, IronSetting as O, parseMachine as P, Variant as S, mixTags as T, washGroups as _, chartToJson as a, MixTag as b, Blocker as c, canMix as d, cardGroups as f, resolve as g, mixBlocker as h, chartFromJson as i, formatTemperature as j, Machine 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, ColourGroup as v, durationsOf as w, ResolvedInstruction as x, Instruction as y };
|
|
198
|
+
//# sourceMappingURL=rows-lRFNdyJh.d.mts.map
|