dubzzz / fast-check

Property based testing framework for JavaScript (like QuickCheck) written in TypeScript
https://fast-check.dev/
MIT License
4.22k stars 176 forks source link

Grapheme-based arbitrary #3406

Open dubzzz opened 1 year ago

dubzzz commented 1 year ago

🚀 Feature Request

Specs rarely talk about code-units or characters. They mostly talk about graphemes.

Let's offer graphemes as first class citizens within fast-check.

Repositories that can help in that direction:

dubzzz commented 1 year ago

Here are some snippets extracting and generating arbitraries out of the official specs of unicode.

Snippet version 3 ```ts // @ts-check /** * @param {string} documentUrl * @returns {Promise<{ sequence: string; kind: string; }[]>} */ async function read(documentUrl) { const response = await fetch(documentUrl); const data = await response.text(); const lines = data .split("\n") .map((line) => line.split("#")[0]) .map((line) => line.split(";")) .map((chunks) => chunks.map((chunk) => chunk.trim())) .filter((chunks) => chunks.length >= 2) .map((chunks) => ({ sequence: chunks[0], kind: chunks[1] })); return lines; } /** * @returns {Promise>} */ async function readAll() { const allItems = [ ...(await read( "https://www.unicode.org/Public/15.0.0/ucd/emoji/emoji-data.txt" )), ...(await read( "https://www.unicode.org/Public/15.0.0/ucd/emoji/emoji-variation-sequences.txt" )), ...(await read( "https://www.unicode.org/Public/emoji/15.0/emoji-sequences.txt" )), ...(await read( "https://www.unicode.org/Public/emoji/15.0/emoji-zwj-sequences.txt" )), ]; /** @type {Map} */ const itemsByCategory = new Map(); for (const item of allItems) { const sequences = itemsByCategory.get(item.kind) ?? []; sequences.push(item.sequence); itemsByCategory.set(item.kind, sequences); } return itemsByCategory; } /** * @param {string[]} firsts * @returns {string} */ function createArbitraryOneOfFirsts(firsts) { const sortedFirsts = firsts .flatMap((f) => { if (!f.includes("..")) { return [f]; } const [from, to] = f.split(".."); const fromCodePoint = parseInt(from, 16); const toCodePoint = parseInt(to, 16); return [...Array(toCodePoint - fromCodePoint + 1)].map((_, i) => Number(fromCodePoint + i).toString(16) ); }) .map((f) => f.padStart(5, "0")) .sort(); if (sortedFirsts.length === 1) { return `fc.constant('${sanitizeOne(sortedFirsts[0])}')`; } /** @type {{num:number, start:number}[]} */ const entries = []; let firstIndex = -1; let firstCodePoint = -1; let lastCodePoint = -1; for (let index = 0; index !== sortedFirsts.length; ++index) { const currentCodePoint = parseInt(sortedFirsts[index], 16); if (currentCodePoint !== lastCodePoint + 1) { if (lastCodePoint !== -1) { entries.push({ num: index - firstIndex, start: firstCodePoint }); } firstIndex = index; firstCodePoint = currentCodePoint; } lastCodePoint = currentCodePoint; } entries.push({ num: sortedFirsts.length - firstIndex, start: firstCodePoint, }); if (entries.reduce((acc, cur) => acc + cur.num, 0) / entries.length < 4) { return `fc.constantFrom(${sortedFirsts.map( (first) => `'${sanitizeOne(first)}'` )})`; } if (entries.length === 1) { const e = entries[0]; return `fc.nat({max:${e.num - 1}}).map(v => String.fromCodePoint(v + ${ e.start }))`; } return `fc.mapToConstant(${entries.map( (e) => `{num: ${e.num}, build: v => String.fromCodePoint(v + ${e.start})}` )})`; } /** * @param {string} one * @returns {string} */ function sanitizeOne(one) { return one.length <= 4 ? `\\u${one.padStart(4, "0")}` : `\\u{${one}}`; } /** * @param {string} trailing * @returns {string} */ function sanitizeTrailing(trailing) { const split = trailing.trim().split(" "); return split.map((t) => sanitizeOne(t)).join(""); } /** * @param {string[]} firsts * @param {string[]} trailings * @returns {string} */ function createArbitraryLine(firsts, trailings) { if (trailings.length === 1) { if (trailings[0].length === 0) { return createArbitraryOneOfFirsts(firsts); } if (firsts.length === 1 && !firsts[0].includes("..")) { return `fc.constant('${sanitizeTrailing( firsts[0] + " " + trailings[0] )}')`; } return `${createArbitraryOneOfFirsts( firsts )}.map(item => item + '${sanitizeTrailing(trailings[0])}')`; } if (firsts.length === 1 && !firsts[0].includes("..")) { return `fc.constantFrom(${trailings.map( (t) => `'${sanitizeTrailing(firsts[0] + " " + t)}'` )})`; } return `fc.tuple(${createArbitraryOneOfFirsts( firsts )}, fc.constantFrom(${trailings .map((t) => `'${sanitizeTrailing(t)}'`) .join(", ")})).map(([f ,t]) => f + t)`; } /** * @param {string} arbitraryName * @param {string[]} dataset * @returns {{code:string,weight:number}} */ function createArbitrary(arbitraryName, dataset) { /** @type {Map} */ const byFirstElement = new Map(); for (const item of dataset) { const [first, ...trailings] = item.split(" "); const trailingsForFirst = byFirstElement.get(first) ?? []; trailingsForFirst.push(trailings.join(" ")); byFirstElement.set(first, trailingsForFirst); } /** @type {Map} */ const byTrailingElements = new Map(); for (const [first, trailings] of byFirstElement) { const trailingKey = [...trailings].sort().join("/"); const firstForTrailingKey = byTrailingElements.get(trailingKey) ?? []; firstForTrailingKey.push(first); byTrailingElements.set(trailingKey, firstForTrailingKey); } /** @type {string} */ let code = `const ${arbitraryName} = fc.oneof(`; for (const [trailingKey, firsts] of byTrailingElements) { const arbitraryLine = createArbitraryLine(firsts, trailingKey.split("/")); const weight = firsts.length; if (weight !== 1) { code += `{ arbitrary: ${arbitraryLine}, weight: ${weight} },`; } else { code += `${arbitraryLine},`; } } code += `);`; return { code, weight: byFirstElement.size }; } /** * @param {string} category * @returns {string} */ function toArbitraryName(category) { const split = category.split(/[ _,]/); return ( split[0].toLocaleLowerCase() + split .slice(1) .map( (e) => `${e[0].toLocaleUpperCase()}${e.substring(1).toLocaleLowerCase()}` ) .join("") + "Arb" ); } readAll().then((itemsByCategory) => { const codes = [...itemsByCategory.entries()].map(([category, dataset]) => { const arbitraryName = toArbitraryName(category); const { code, weight } = createArbitrary(arbitraryName, dataset); return { name: arbitraryName, code, weight }; }); console.log(`import fc from "fast-check";`); codes.forEach(({ code }) => console.log(code)); console.log( `export const anyArb = fc.oneof(${codes .map(({ name, weight }) => `{ arbitrary: ${name}, weight: ${weight} }`) .join(",")});` ); console.log( `export const rawItems = [${[...itemsByCategory.values()] .flat() .flatMap((v) => { if (!v.includes("..")) { return [v]; } const [from, to] = v.split(".."); const fromCodePoint = parseInt(from, 16); const toCodePoint = parseInt(to, 16); return [...Array(toCodePoint - fromCodePoint + 1)].map((_, i) => Number(fromCodePoint + i) .toString(16) .padStart(4, "0") .toUpperCase() ); }) .map((v) => JSON.stringify(v))}];` ); }); ```
Snippet version 2 ```js // @ts-check /** * @param {string} documentUrl * @returns {Promise<{ sequence: string; kind: string; }[]>} */ async function read(documentUrl) { const response = await fetch(documentUrl); const data = await response.text(); const lines = data .split("\n") .map((line) => line.split("#")[0]) .map((line) => line.split(";")) .map((chunks) => chunks.map((chunk) => chunk.trim())) .filter((chunks) => chunks.length >= 2) .map((chunks) => ({ sequence: chunks[0], kind: chunks[1] })); return lines; } /** * @returns {Promise>} */ async function readAll() { const allItems = [ ...(await read( "https://www.unicode.org/Public/15.0.0/ucd/emoji/emoji-data.txt" )), ...(await read( "https://www.unicode.org/Public/15.0.0/ucd/emoji/emoji-variation-sequences.txt" )), ...(await read( "https://www.unicode.org/Public/emoji/15.0/emoji-sequences.txt" )), ...(await read( "https://www.unicode.org/Public/emoji/15.0/emoji-zwj-sequences.txt" )), ]; /** @type {Map} */ const itemsByCategory = new Map(); for (const item of allItems) { const sequences = itemsByCategory.get(item.kind) ?? []; sequences.push(item.sequence); itemsByCategory.set(item.kind, sequences); } return itemsByCategory; } /** * @param {string[]} firsts * @returns {string} */ function createArbitraryOneOfFirsts(firsts) { const sortedFirsts = firsts .flatMap((f) => { if (!f.includes("..")) { return [f]; } const [from, to] = f.split(".."); const fromCodePoint = parseInt(from, 16); const toCodePoint = parseInt(to, 16); return [...Array(toCodePoint - fromCodePoint + 1)].map((_, i) => Number(fromCodePoint + i).toString(16) ); }) .map((f) => f.padStart(5, "0")) .sort(); if (sortedFirsts.length === 1) { return `fc.constant('${sanitizeOne(sortedFirsts[0])}')`; } /** @type {{num:number, start:number}[]} */ const entries = []; let firstIndex = -1; let firstCodePoint = -1; let lastCodePoint = -1; for (let index = 0; index !== sortedFirsts.length; ++index) { const currentCodePoint = parseInt(sortedFirsts[index], 16); if (currentCodePoint !== lastCodePoint + 1) { if (lastCodePoint !== -1) { entries.push({ num: index - firstIndex, start: firstCodePoint }); } firstIndex = index; firstCodePoint = currentCodePoint; } lastCodePoint = currentCodePoint; } entries.push({ num: sortedFirsts.length - firstIndex, start: firstCodePoint, }); if (entries.reduce((acc, cur) => acc + cur.num, 0) / entries.length < 4) { return `fc.constantFrom(${sortedFirsts.map( (first) => `'${sanitizeOne(first)}'` )})`; } if (entries.length === 1) { const e = entries[0]; return `fc.nat({max:${e.num - 1}}).map(v => String.fromCharCode(v + ${ e.start }))`; } return `fc.mapToConstant(${entries.map( (e) => `{num: ${e.num}, build: v => String.fromCharCode(v + ${e.start})}` )})`; } /** * @param {string} one * @returns {string} */ function sanitizeOne(one) { return one.length <= 4 ? `\\u${one.padStart(4, "0")}` : `\\u{${one}}`; } /** * @param {string} trailing * @returns {string} */ function sanitizeTrailing(trailing) { const split = trailing.trim().split(" "); return split.map((t) => sanitizeOne(t)).join(""); } /** * @param {string[]} firsts * @param {string[]} trailings * @returns {string} */ function createArbitraryLine(firsts, trailings) { if (trailings.length === 1) { if (trailings[0].length === 0) { return createArbitraryOneOfFirsts(firsts); } if (firsts.length === 1 && !firsts[0].includes("..")) { return `fc.constant('${sanitizeTrailing( firsts[0] + " " + trailings[0] )}')`; } return `${createArbitraryOneOfFirsts( firsts )}.map(item => item + '${sanitizeTrailing(trailings[0])}')`; } if (firsts.length === 1 && !firsts[0].includes("..")) { return `fc.constantFrom(${trailings.map( (t) => `'${sanitizeTrailing(firsts[0] + " " + t)}'` )})`; } return `fc.tuple(${createArbitraryOneOfFirsts( firsts )}, fc.constantFrom(${trailings .map((t) => `'${sanitizeTrailing(t)}'`) .join(", ")})).map(([f ,t]) => f + t)`; } /** * @param {string} arbitraryName * @param {string[]} dataset * @returns {{code:string,weight:number}} */ function createArbitrary(arbitraryName, dataset) { /** @type {Map} */ const byFirstElement = new Map(); for (const item of dataset) { const [first, ...trailings] = item.split(" "); const trailingsForFirst = byFirstElement.get(first) ?? []; trailingsForFirst.push(trailings.join(" ")); byFirstElement.set(first, trailingsForFirst); } /** @type {Map} */ const byTrailingElements = new Map(); for (const [first, trailings] of byFirstElement) { const trailingKey = [...trailings].sort().join("/"); const firstForTrailingKey = byTrailingElements.get(trailingKey) ?? []; firstForTrailingKey.push(first); byTrailingElements.set(trailingKey, firstForTrailingKey); } /** @type {string} */ let code = `const ${arbitraryName} = fc.oneof(`; for (const [trailingKey, firsts] of byTrailingElements) { const arbitraryLine = createArbitraryLine(firsts, trailingKey.split("/")); const weight = firsts.length; if (weight !== 1) { code += `{ arbitrary: ${arbitraryLine}, weight: ${weight} },`; } else { code += `${arbitraryLine},`; } } code += `);`; return { code, weight: byFirstElement.size }; } /** * @param {string} category * @returns {string} */ function toArbitraryName(category) { const split = category.split(/[ _,]/); return ( split[0].toLocaleLowerCase() + split .slice(1) .map( (e) => `${e[0].toLocaleUpperCase()}${e.substring(1).toLocaleLowerCase()}` ) .join("") + "Arb" ); } readAll().then((itemsByCategory) => { const codes = [...itemsByCategory.entries()].map(([category, dataset]) => { const arbitraryName = toArbitraryName(category); const { code, weight } = createArbitrary(arbitraryName, dataset); return { name: arbitraryName, code, weight }; }); codes.forEach(({ code }) => console.log(code)); console.log( `const anyArb = fc.oneof(${codes .map(({ name, weight }) => `{ arbitrary: ${name}, weight: ${weight} }`) .join(",")});` ); }); ```
Snippet version 1 ```js // @ts-check /** * @param {string} documentUrl * @returns {Promise<{ sequence: string; kind: string; }[]>} */ async function read(documentUrl) { const response = await fetch(documentUrl); const data = await response.text(); const lines = data .split("\n") .map((line) => line.split("#")[0]) .map((line) => line.split(";")) .map((chunks) => chunks.map((chunk) => chunk.trim())) .filter((chunks) => chunks.length >= 2) .map((chunks) => ({ sequence: chunks[0], kind: chunks[1] })); return lines; } /** * @returns {Promise>} */ async function readAll() { const allItems = [ ...(await read( "https://www.unicode.org/Public/15.0.0/ucd/emoji/emoji-data.txt" )), ...(await read( "https://www.unicode.org/Public/15.0.0/ucd/emoji/emoji-variation-sequences.txt" )), ...(await read( "https://www.unicode.org/Public/emoji/15.0/emoji-sequences.txt" )), ...(await read( "https://www.unicode.org/Public/emoji/15.0/emoji-zwj-sequences.txt" )), ]; /** @type {Map} */ const itemsByCategory = new Map(); for (const item of allItems) { const sequences = itemsByCategory.get(item.kind) ?? []; sequences.push(item.sequence); itemsByCategory.set(item.kind, sequences); } return itemsByCategory; } /** * @param {string[]} firsts * @returns {string} */ function createArbitraryOneOfFirsts(firsts) { const sortedFirsts = firsts .flatMap((f) => { if (!f.includes("..")) { return [f]; } const [from, to] = f.split(".."); const fromCodePoint = parseInt(from, 16); const toCodePoint = parseInt(to, 16); return [...Array(toCodePoint - fromCodePoint + 1)].map((_, i) => Number(fromCodePoint + i).toString(16) ); }) .map((f) => f.padStart(5, "0")) .sort(); if (sortedFirsts.length === 1) { return `fc.constant('${sanitizeOne(sortedFirsts[0])}')`; } /** @type {{num:number, start:number}[]} */ const entries = []; let firstIndex = -1; let firstCodePoint = -1; let lastCodePoint = -1; for (let index = 0; index !== sortedFirsts.length; ++index) { const currentCodePoint = parseInt(sortedFirsts[index], 16); if (currentCodePoint !== lastCodePoint + 1) { if (lastCodePoint !== -1) { entries.push({ num: index - firstIndex, start: firstCodePoint }); } firstIndex = index; firstCodePoint = currentCodePoint; } lastCodePoint = currentCodePoint; } entries.push({ num: sortedFirsts.length - firstIndex, start: firstCodePoint, }); if (entries.length === 1) { const e = entries[0]; return `fc.nat({max:${e.num - 1}}).map(v => String.fromCharCode(v + ${ e.start }))`; } return `fc.mapToConstant(${entries.map( (e) => `{num: ${e.num}, build: v => String.fromCharCode(v + ${e.start})}` )})`; } /** * @param {string} one * @returns {string} */ function sanitizeOne(one) { return one.length <= 4 ? `\\u${one.padStart(4, "0")}` : `\\u{${one}}`; } /** * @param {string} trailing * @returns {string} */ function sanitizeTrailing(trailing) { const split = trailing.trim().split(" "); return split.map((t) => sanitizeOne(t)).join(""); } /** * @param {string[]} firsts * @param {string[]} trailings * @returns {string} */ function createArbitraryLine(firsts, trailings) { if (trailings.length === 1) { if (trailings[0].length === 0) { return createArbitraryOneOfFirsts(firsts); } if (firsts.length === 1 && !firsts[0].includes("..")) { return `fc.constant('${sanitizeTrailing( firsts[0] + " " + trailings[0] )}')`; } return `${createArbitraryOneOfFirsts( firsts )}.map(item => item + '${sanitizeTrailing(trailings[0])}')`; } if (firsts.length === 1 && !firsts[0].includes("..")) { return `fc.constantFrom(${trailings.map( (t) => `'${sanitizeTrailing(firsts[0] + " " + t)}'` )})`; } return `fc.tuple(${createArbitraryOneOfFirsts( firsts )}, fc.constantFrom(${trailings .map((t) => `'${sanitizeTrailing(t)}'`) .join(", ")})).map(([f ,t]) => f + t)`; } /** * @param {string} arbitraryName * @param {string[]} dataset * @returns {{code:string,weight:number}} */ function createArbitrary(arbitraryName, dataset) { /** @type {Map} */ const byFirstElement = new Map(); for (const item of dataset) { const [first, ...trailings] = item.split(" "); const trailingsForFirst = byFirstElement.get(first) ?? []; trailingsForFirst.push(trailings.join(" ")); byFirstElement.set(first, trailingsForFirst); } /** @type {Map} */ const byTrailingElements = new Map(); for (const [first, trailings] of byFirstElement) { const trailingKey = [...trailings].sort().join("/"); const firstForTrailingKey = byTrailingElements.get(trailingKey) ?? []; firstForTrailingKey.push(first); byTrailingElements.set(trailingKey, firstForTrailingKey); } /** @type {string} */ let code = `const ${arbitraryName} = fc.oneof(`; for (const [trailingKey, firsts] of byTrailingElements) { const arbitraryLine = createArbitraryLine(firsts, trailingKey.split("/")); const weight = firsts.length; if (weight !== 1) { code += `{ arbitrary: ${arbitraryLine}, weight: ${weight} },`; } else { code += `${arbitraryLine},`; } } code += `);`; return { code, weight: byFirstElement.size }; } /** * @param {string} category * @returns {string} */ function toArbitraryName(category) { const split = category.split(/[ _,]/); return ( split[0].toLocaleLowerCase() + split .slice(1) .map( (e) => `${e[0].toLocaleUpperCase()}${e.substring(1).toLocaleLowerCase()}` ) .join("") + "Arb" ); } readAll().then((itemsByCategory) => { const codes = [...itemsByCategory.entries()].map(([category, dataset]) => { const arbitraryName = toArbitraryName(category); const { code, weight } = createArbitrary(arbitraryName, dataset); return { name: arbitraryName, code, weight }; }); codes.forEach(({ code }) => console.log(code)); console.log( `const anyArb = fc.oneof(${codes .map(({ name, weight }) => `{ arbitrary: ${name}, weight: ${weight} }`) .join(",")});` ); }); ```
Snippet version 0 ```js // @ts-check /** * @param {string} documentUrl * @returns {Promise<{ sequence: string; kind: string; }[]>} */ async function read(documentUrl) { const response = await fetch(documentUrl); const data = await response.text(); const lines = data .split("\n") .map((line) => line.split("#")[0]) .map((line) => line.split(";")) .map((chunks) => chunks.map((chunk) => chunk.trim())) .filter((chunks) => chunks.length >= 2) .map((chunks) => ({ sequence: chunks[0], kind: chunks[1] })); return lines; } /** * @returns {Promise>} */ async function readAll() { const allItems = [ ...(await read( "https://www.unicode.org/Public/15.0.0/ucd/emoji/emoji-data.txt" )), ...(await read( "https://www.unicode.org/Public/15.0.0/ucd/emoji/emoji-variation-sequences.txt" )), ...(await read( "https://www.unicode.org/Public/emoji/15.0/emoji-sequences.txt" )), ...(await read( "https://www.unicode.org/Public/emoji/15.0/emoji-zwj-sequences.txt" )), ]; /** @type {Map} */ const itemsByCategory = new Map(); for (const item of allItems) { const sequences = itemsByCategory.get(item.kind) ?? []; sequences.push(item.sequence); itemsByCategory.set(item.kind, sequences); } return itemsByCategory; } /** * @param {string[]} firsts * @returns {string} */ function createArbitraryOneOfFirsts(firsts) { const sortedFirsts = firsts .flatMap((f) => { if (!f.includes("..")) { return [f]; } const [from, to] = f.split(".."); const fromCodePoint = parseInt(from, 16); const toCodePoint = parseInt(to, 16); return [...Array(toCodePoint - fromCodePoint + 1)].map((_, i) => Number(fromCodePoint + i).toString(16) ); }) .map((f) => f.padStart(5, "0")) .sort(); if (sortedFirsts.length === 1) { return `fc.constant('${sanitizeOne(sortedFirsts[0])}')`; } /** @type {{num:number, start:number}[]} */ const entries = []; let firstIndex = -1; let firstCodePoint = -1; let lastCodePoint = -1; for (let index = 0; index !== sortedFirsts.length; ++index) { const currentCodePoint = parseInt(sortedFirsts[index], 16); if (currentCodePoint !== lastCodePoint + 1) { if (lastCodePoint !== -1) { entries.push({ num: index - firstIndex, start: firstCodePoint }); } firstIndex = index; firstCodePoint = currentCodePoint; } lastCodePoint = currentCodePoint; } entries.push({ num: sortedFirsts.length - firstIndex, start: firstCodePoint, }); if (entries.length === 1) { const e = entries[0]; return `fc.nat({max:${e.num - 1}}).map(v => String.fromCharCode(v + ${ e.start }))`; } return `fc.mapToConstant(${entries.map( (e) => `{num: ${e.num}, build: v => String.fromCharCode(v + ${e.start})}` )})`; } /** * @param {string} one * @returns {string} */ function sanitizeOne(one) { return one.length <= 4 ? `\\u${one.padStart(4, "0")}` : `\\u{${one}}`; } /** * @param {string} trailing * @returns {string} */ function sanitizeTrailing(trailing) { const split = trailing.trim().split(" "); return split.map((t) => sanitizeOne(t)).join(""); } /** * @param {string[]} firsts * @param {string[]} trailings * @returns {string} */ function createArbitraryLine(firsts, trailings) { if (trailings.length === 1) { if (trailings[0].length === 0) { return createArbitraryOneOfFirsts(firsts); } if (firsts.length === 1 && !firsts[0].includes("..")) { return `fc.constant('${sanitizeTrailing( firsts[0] + " " + trailings[0] )}')`; } return `${createArbitraryOneOfFirsts( firsts )}.map(item => item + '${sanitizeTrailing(trailings[0])}')`; } if (firsts.length === 1 && !firsts[0].includes("..")) { return `fc.constantFrom(${trailings.map( (t) => `'${sanitizeTrailing(firsts[0] + " " + t)}'` )})`; } return `fc.tuple(${createArbitraryOneOfFirsts( firsts )}, fc.constantFrom(${trailings .map((t) => `'${sanitizeTrailing(t)}'`) .join(", ")})).map(([f ,t]) => f + t)`; } /** * @param {string} arbitraryName * @param {string[]} dataset * @returns {string} */ function createArbitrary(arbitraryName, dataset) { /** @type {Map} */ const byFirstElement = new Map(); for (const item of dataset) { const [first, ...trailings] = item.split(" "); const trailingsForFirst = byFirstElement.get(first) ?? []; trailingsForFirst.push(trailings.join(" ")); byFirstElement.set(first, trailingsForFirst); } /** @type {Map} */ const byTrailingElements = new Map(); for (const [first, trailings] of byFirstElement) { const trailingKey = [...trailings].sort().join("/"); const firstForTrailingKey = byTrailingElements.get(trailingKey) ?? []; firstForTrailingKey.push(first); byTrailingElements.set(trailingKey, firstForTrailingKey); } /** @type {string} */ let code = `const ${arbitraryName} = fc.oneof(`; for (const [trailingKey, firsts] of byTrailingElements) { const arbitraryLine = createArbitraryLine(firsts, trailingKey.split("/")); const weight = firsts.length; if (weight !== 1) { code += `{ arbitrary: ${arbitraryLine}, weight: ${weight} },`; } else { code += `${arbitraryLine},`; } } code += `);`; return code; } /** * @param {string} category * @returns {string} */ function toArbitraryName(category) { const split = category.split(/[ _,]/); return ( split[0].toLocaleLowerCase() + split .slice(1) .map( (e) => `${e[0].toLocaleUpperCase()}${e.substring(1).toLocaleLowerCase()}` ) .join("") ); } readAll().then((itemsByCategory) => { [...itemsByCategory.entries()].map(([category, dataset]) => { console.log(createArbitrary(toArbitraryName(category), dataset)); }); }); ```