Add step tree dispaly.
This commit is contained in:
parent
458f217c30
commit
4aab03fd98
5 changed files with 286 additions and 74 deletions
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"fmt": {
|
"fmt": {
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"lineWidth": 120
|
"lineWidth": 120,
|
||||||
}
|
"semiColons": false
|
||||||
|
}
|
||||||
}
|
}
|
11
example/failing
Normal file
11
example/failing
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
arbre
|
||||||
|
1 mort
|
||||||
|
|
||||||
|
mort
|
||||||
|
2 feur
|
||||||
|
|
||||||
|
feur
|
||||||
|
1 arbre
|
||||||
|
|
||||||
|
1 feur
|
||||||
|
1 barbe
|
48
example/mc-induction-cell
Normal file
48
example/mc-induction-cell
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
1 elite induction cell
|
||||||
|
|
||||||
|
elite induction cell
|
||||||
|
1 elite energy cube
|
||||||
|
4 advanced induction cell
|
||||||
|
4 energy tablet
|
||||||
|
|
||||||
|
advanced induction cell
|
||||||
|
1 advanced energy cube
|
||||||
|
4 basic induction cell
|
||||||
|
4 energy tablet
|
||||||
|
|
||||||
|
basic induction cell
|
||||||
|
1 basic energy cube
|
||||||
|
4 lithium dust
|
||||||
|
4 energy tablet
|
||||||
|
|
||||||
|
elite energy cube
|
||||||
|
1 advanced energy cube
|
||||||
|
4 reinforced alloy
|
||||||
|
2 gold ingot
|
||||||
|
2 energy tablet
|
||||||
|
|
||||||
|
advanced energy cube
|
||||||
|
1 basic energy cube
|
||||||
|
4 enriched alloy
|
||||||
|
2 osmium ingot
|
||||||
|
2 energy tablet
|
||||||
|
|
||||||
|
basic energy cube
|
||||||
|
1 steel casing
|
||||||
|
4 redstrone dust
|
||||||
|
2 iron ingot
|
||||||
|
2 energy tablet
|
||||||
|
|
||||||
|
steel casing
|
||||||
|
1 osmium ingot
|
||||||
|
4 steel ingot
|
||||||
|
4 glass block
|
||||||
|
|
||||||
|
energy tablet
|
||||||
|
3 gold ingot
|
||||||
|
2 enriched alloy
|
||||||
|
4 redstrone dust
|
||||||
|
|
||||||
|
reinforced alloy
|
||||||
|
1 enriched alloy
|
||||||
|
0.125 diamond
|
95
src/main.ts
95
src/main.ts
|
@ -1,79 +1,36 @@
|
||||||
#!/bin/env -S deno run --allow-read
|
#!/bin/env -S deno run --allow-read
|
||||||
|
|
||||||
|
import { CircDepErr, RecipeSet, Step } from "./tree.ts"
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const { path } = parse_args(Deno.args);
|
const { path } = parse_args(Deno.args)
|
||||||
const content = await Deno.readTextFile(path);
|
const content = await Deno.readTextFile(path)
|
||||||
const { targets, definitions } = parse_file(content);
|
const [targets, recipes] = RecipeSet.parse(content)
|
||||||
const ingredients = calculate_ingredients(targets, definitions);
|
const steps = Step.resolve(targets, recipes)
|
||||||
const merged = merge_ingredients(ingredients);
|
if (steps instanceof CircDepErr) return console.error("Circular recipe detected", steps.path)
|
||||||
console.log("To produce :\n", ...format_ingredients(targets), "\nGet :\n", ...format_ingredients(merged));
|
report_steps(steps)
|
||||||
}
|
}
|
||||||
|
|
||||||
function parse_args(args: string[]) {
|
function parse_args(args: string[]) {
|
||||||
const invalid_usage = () => Deno.exit(console.log("Usage:\nreciper <recipe_file>") as undefined);
|
const invalid_usage = () => Deno.exit(console.log("Usage:\nreciper <recipe_file>") as undefined)
|
||||||
if (args.length !== 1) invalid_usage();
|
if (args.length !== 1) invalid_usage()
|
||||||
const [path] = args;
|
const [path] = args
|
||||||
if (["-h", "--help"].includes(path)) invalid_usage();
|
if (["-h", "--help"].includes(path)) invalid_usage()
|
||||||
return { path };
|
return { path }
|
||||||
}
|
}
|
||||||
|
|
||||||
function parse_file(content: string) {
|
function report_steps(steps: Step) {
|
||||||
const lines = content.trim().split("\n");
|
console.log("Steps")
|
||||||
const definitions = [] as Definition[];
|
console.log()
|
||||||
const main = { target: "main", ingredients: [] } as Definition;
|
console.log(steps.display())
|
||||||
let current_block = null as Definition | null;
|
console.log()
|
||||||
for (const line of lines) {
|
console.log("Intermediaries")
|
||||||
if (line.trim().startsWith("#")) continue;
|
console.log()
|
||||||
if (line.trim() === "") {
|
console.log(steps.intermediaries().map(([name, amount]) => `${amount}\t${name}`).join("\n"))
|
||||||
if (current_block !== null) definitions.push(current_block);
|
console.log()
|
||||||
current_block = null;
|
console.log("Inputs")
|
||||||
continue;
|
console.log()
|
||||||
}
|
console.log(steps.inputs().map(([name, amount]) => `${amount}\t${name}`).join("\n"))
|
||||||
const [count_str, ...words] = line.split(" ");
|
|
||||||
const count = parseFloat(count_str);
|
|
||||||
if (isNaN(count)) {
|
|
||||||
if (current_block !== null) definitions.push(current_block);
|
|
||||||
current_block = null;
|
|
||||||
current_block = { target: line, ingredients: [] };
|
|
||||||
} else {
|
|
||||||
const item = words.join(" ");
|
|
||||||
if (current_block === null) main.ingredients.push({ item, count });
|
|
||||||
else current_block.ingredients.push({ item, count });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current_block !== null) definitions.push(current_block);
|
|
||||||
const targets = main.ingredients;
|
|
||||||
return { targets, definitions };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Ingredient = { item: string; count: number };
|
if (import.meta.main) await main()
|
||||||
type Definition = { target: string; ingredients: Ingredient[] };
|
|
||||||
|
|
||||||
function calculate_ingredients(targets: Ingredient[], definitions: Definition[]) {
|
|
||||||
const result = [] as Ingredient[];
|
|
||||||
for (const target of targets) {
|
|
||||||
const recipe = definitions.find((recipe) => recipe.target === target.item);
|
|
||||||
if (recipe === undefined) {
|
|
||||||
result.push(target);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const defs = definitions.filter((def) => def.target !== target.item);
|
|
||||||
for (const ingredient of calculate_ingredients(recipe.ingredients, defs)) {
|
|
||||||
const count = ingredient.count * target.count;
|
|
||||||
result.push({ item: ingredient.item, count });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function merge_ingredients(ingredients: Ingredient[]): Ingredient[] {
|
|
||||||
const set = new Map<string, number>();
|
|
||||||
for (const { count, item } of ingredients) set.set(item, count + (set.get(item) ?? 0));
|
|
||||||
return Array.from(set.entries()).map(([item, count]) => ({ item, count }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function* format_ingredients(ingredients: Ingredient[]) {
|
|
||||||
for (const { count, item } of ingredients) yield* ["-", count, item, "\n"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.main) await main();
|
|
||||||
|
|
195
src/tree.ts
Normal file
195
src/tree.ts
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"
|
||||||
|
|
||||||
|
export class Ingredient {
|
||||||
|
public constructor(
|
||||||
|
public name: string,
|
||||||
|
public amount: number,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Recipe {
|
||||||
|
public constructor(
|
||||||
|
public name: string,
|
||||||
|
public ingredients: Ingredient[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RecipeSet {
|
||||||
|
public constructor(
|
||||||
|
public recipes = new Map<string, Recipe>(),
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public append(recipe: Recipe) {
|
||||||
|
this.recipes.set(recipe.name, recipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static parse(text: string) {
|
||||||
|
const main = new Recipe("main", [])
|
||||||
|
const result = new RecipeSet()
|
||||||
|
let current_block = null as Recipe | null
|
||||||
|
function append_current() {
|
||||||
|
if (current_block !== null) result.append(current_block)
|
||||||
|
current_block = null
|
||||||
|
}
|
||||||
|
for (const line of text.split("\n")) {
|
||||||
|
if (line.trim().startsWith("#")) continue
|
||||||
|
if (line.trim() === "") {
|
||||||
|
append_current()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const [count_str, ...words] = line.split(" ")
|
||||||
|
const count = parseFloat(count_str)
|
||||||
|
if (isNaN(count)) {
|
||||||
|
append_current()
|
||||||
|
current_block = new Recipe(line, [])
|
||||||
|
} else {
|
||||||
|
const ingredient = new Ingredient(words.join(" "), count)
|
||||||
|
if (current_block === null) main.ingredients.push(ingredient)
|
||||||
|
else current_block.ingredients.push(ingredient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append_current()
|
||||||
|
return [main, result] as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("test_recipe_set_parse", () => {
|
||||||
|
const [main, recipe_set] = RecipeSet.parse(`
|
||||||
|
a
|
||||||
|
1 b
|
||||||
|
|
||||||
|
b
|
||||||
|
2 c
|
||||||
|
|
||||||
|
2 a
|
||||||
|
1 c
|
||||||
|
`)
|
||||||
|
assertEquals(
|
||||||
|
recipe_set,
|
||||||
|
new RecipeSet(
|
||||||
|
new Map([
|
||||||
|
["a", new Recipe("a", [new Ingredient("b", 1)])],
|
||||||
|
["b", new Recipe("b", [new Ingredient("c", 2)])],
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(main, new Recipe("main", [new Ingredient("a", 2), new Ingredient("c", 1)]))
|
||||||
|
})
|
||||||
|
|
||||||
|
export class Step {
|
||||||
|
public constructor(
|
||||||
|
public name: string,
|
||||||
|
public amount: number,
|
||||||
|
public deps: Step[],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static resolve(root: Recipe, recipes: RecipeSet): Step | CircDepErr {
|
||||||
|
function recurse(recipe: Recipe, multiplier: number, path: string[]): Step | CircDepErr {
|
||||||
|
const next_parents = [...path, recipe.name]
|
||||||
|
const steps = [] as Step[]
|
||||||
|
for (const ingr of recipe.ingredients) {
|
||||||
|
if (next_parents.includes(ingr.name)) return new CircDepErr(next_parents)
|
||||||
|
const ingr_multiplier = ingr.amount * multiplier
|
||||||
|
const ingr_recipe = recipes.recipes.get(ingr.name)
|
||||||
|
if (ingr_recipe === undefined) {
|
||||||
|
steps.push(new Step(ingr.name, ingr_multiplier, []))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const res = recurse(ingr_recipe, ingr_multiplier, next_parents)
|
||||||
|
if (res instanceof CircDepErr) return res
|
||||||
|
steps.push(res)
|
||||||
|
}
|
||||||
|
return new Step(recipe.name, multiplier, steps)
|
||||||
|
}
|
||||||
|
return recurse(root, 1, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
public display() {
|
||||||
|
function* rec(step: Step): Generator<string> {
|
||||||
|
yield `${step.amount} ${step.name}`
|
||||||
|
const deps = [...step.deps]
|
||||||
|
if (deps.length < 1) return
|
||||||
|
const [last] = deps.splice(deps.length - 1, 1)
|
||||||
|
for (const dep of deps) {
|
||||||
|
const [first, ...lines] = rec(dep)
|
||||||
|
yield `├─${first}`
|
||||||
|
for (const line of lines) yield `│ ${line}`
|
||||||
|
}
|
||||||
|
const [first, ...lines] = rec(last)
|
||||||
|
yield `└─${first}`
|
||||||
|
for (const line of lines) yield ` ${line}`
|
||||||
|
}
|
||||||
|
return [...this.deps.map(rec).map((l) => l.toArray())].flat().join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
public intermediaries() {
|
||||||
|
function* rec(step: Step): Generator<[string, number]> {
|
||||||
|
if (step.deps.length > 0) yield [step.name, step.amount]
|
||||||
|
for (const dep of step.deps) yield* rec(dep)
|
||||||
|
}
|
||||||
|
const result = new DefaultMap((_: string) => ({ total: 0 }))
|
||||||
|
for (const dep of this.deps) for (const [name, value] of rec(dep)) result.get(name).total += value
|
||||||
|
return result.map.entries()
|
||||||
|
.map(([k, { total }]) => [k, total] as const)
|
||||||
|
.toArray()
|
||||||
|
.toSorted(([a], [b]) => a.localeCompare(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
public inputs() {
|
||||||
|
function* rec(step: Step): Generator<[string, number]> {
|
||||||
|
if (step.deps.length < 1) yield [step.name, step.amount]
|
||||||
|
for (const dep of step.deps) yield* rec(dep)
|
||||||
|
}
|
||||||
|
const result = new DefaultMap((_: string) => ({ total: 0 }))
|
||||||
|
for (const dep of this.deps) for (const [name, value] of rec(dep)) result.get(name).total += value
|
||||||
|
return result.map.entries()
|
||||||
|
.map(([k, { total }]) => [k, total] as const)
|
||||||
|
.toArray()
|
||||||
|
.toSorted(([a], [b]) => a.localeCompare(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CircDepErr {
|
||||||
|
public constructor(
|
||||||
|
public path: string[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("test_display_lines", () => {
|
||||||
|
const [main, recipe_set] = RecipeSet.parse(`
|
||||||
|
a
|
||||||
|
1 b
|
||||||
|
1 c
|
||||||
|
|
||||||
|
b
|
||||||
|
2 c
|
||||||
|
|
||||||
|
2 a
|
||||||
|
1 c
|
||||||
|
`.trim())
|
||||||
|
const root = Step.resolve(main, recipe_set)
|
||||||
|
if (root instanceof CircDepErr) throw new Error()
|
||||||
|
const expects = `
|
||||||
|
1 main
|
||||||
|
├─2 a
|
||||||
|
│ ├─2 b
|
||||||
|
│ │ └─4 c
|
||||||
|
│ └─2 c
|
||||||
|
└─1 c
|
||||||
|
`.trim()
|
||||||
|
assertEquals(root.display(), expects)
|
||||||
|
})
|
||||||
|
|
||||||
|
export class DefaultMap<K, V> {
|
||||||
|
public map = new Map<K, V>()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public default_construct: (key: K) => V,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get(key: K) {
|
||||||
|
if (!this.map.has(key)) this.map.set(key, this.default_construct(key))
|
||||||
|
return this.map.get(key) as V
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue