-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcountdown.js
More file actions
156 lines (146 loc) · 6.01 KB
/
countdown.js
File metadata and controls
156 lines (146 loc) · 6.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"use strict";
// Partition an array according to a boolean vector and maintain ordering of
// array elements
function partition(xs, selection) {
const ls = [];
const rs = [];
for (let i = 0; i < selection.length; i++) {
(selection[i] ? ls : rs).push(xs[i]);
}
return [ls, rs];
}
// Generate all boolean vectors of length n, omitting all false and all true
function* selections(n) {
const fs = [];
for (let i = 0; i < n; i++) {
fs.push(false);
}
// There are 2^n possible vectors
const max = Math.pow(2, n) - 2;
for (let _ = 0; _ < max; _++) {
let carry = true;
let j = 0;
while (carry) {
carry = fs[j];
fs[j] = !fs[j];
j++;
}
yield fs;
}
}
// Generate all terms that can be made from given numbers. Expects the array of
// numbers to be sorted. With optimizations in the term generation inspired by
// http://www.datagenetics.com/blog/august32014/index.html.
// TODO avoid (... * a * ...) / a but not a / a = 1
// TODO avoid (... + a + ...) - a
// TODO avoid duplicate terms when number is in selection more than once
function* terms(numbers) {
if (numbers.length === 1) {
yield { op: "#", args: numbers, val: numbers[0] };
return;
}
// Split arguments into all possible variations of two subsets and combine
// these subsets with the binary operators - and /
for (let selection of selections(numbers.length)) {
const [ls, rs] = partition(numbers, selection);
for (let l of terms(ls)) {
for (let r of terms(rs)) {
// Addition: to avoid duplicates, arguments are ordered from
// smallest to largest value, merged into n-ary sums from only
// one direction and subtraction is never inluded in a sum
// because (a - b) + c = (a + c) - b
if (l.op !== "+" && l.op !== "-" && r.op !== "-") {
if (r.op === "+") {
if (l.val <= r.args[0].val) {
yield { op: "+", args: [l, ...r.args], val: l.val + r.val };
}
} else if (l.val <= r.val) {
yield { op: "+", args: [l, r], val: l.val + r.val };
}
}
// Multiplication: to avoid duplicates, arguments are ordered
// from smallest to largest value, merged into n-ary products
// from only one direction and division is never inluded in
// a sum because (a / b) * c = (a * c) / b
if (l.op !== "*" && l.op !== "/" && r.op !== "/" && l.val !== 1 && r.val !== 1) {
if (r.op === "*") {
if (l.val <= r.args[0].val) {
yield { op: "*", args: [l, ...r.args], val: l.val * r.val };
}
} else if (l.val <= r.val) {
yield { op: "*", args: [l, r], val: l.val * r.val };
}
}
// Subtraction: binary only because a - b - c = a - (b + c)
if (l.op !== "-" && r.op !== "-") {
const val = l.val - r.val;
// Don't create 0 or negative numbers and skip a - b = b
if (val > 0 && val !== r.val) {
yield { op: "-", args: [l, r], val: val };
}
}
// Division: binary only because a / b / c = a / (b * c)
// Skip a / b = a
if (l.op !== "/" && r.op !== "/" && r.val > 1) {
const val = l.val / r.val;
// Don't create non-integer numbers and skip a / b = b
if (Number.isInteger(val) && val !== r.val) {
yield { op: "/", args: [l, r], val: val };
}
}
}
}
}
}
// Generate all possible calculations that can be made with the given set of
// numbers as well as its subsets
function* countdown(numbers) {
// Numbers in ascending order
const sortedNumbers = Array.from(numbers).sort((x, y) => x - y);
// Filter out same subsets when a number is given twice
const subsets = new Map();
for (let selection of selections(sortedNumbers.length)) {
const subset = partition(sortedNumbers, selection)[0];
subsets.set(subset.join(","), subset);
}
subsets.set(sortedNumbers.join(","), sortedNumbers);
// Try to find short solutions first
const sortedSubsets = Array.from(subsets.values()).sort((x, y) => x.length - y.length);
for (let subset of sortedSubsets) {
yield* terms(subset);
}
}
// Export countdown function if used as module
if (typeof module !== "undefined") {
module.exports = countdown;
}
// Basic operator precedence rules
const precedence = { "+": 1, "-": 2, "*": 3, "/": 4 };
// Pretty printer
function termToString(term, parentPrecedence) {
if (term.op === "#") {
return term.val.toString();
}
const out = term.args.map((_) => termToString(_, precedence[term.op])).join(term.op);
const parentheses = parentPrecedence != null && parentPrecedence > precedence[term.op];
return parentheses ? "(" + out + ")" : out;
}
// Command line interface
if (typeof require !== "undefined" && require.main === module) {
const args = process.argv.slice(2).map((_) => parseInt(_, 10));
if (args.length < 2 || args.map(isNaN).reduce((p, q) => p || q)) {
console.log("countdown.js target number [number...]");
} else {
const [target, ...numbers] = args;
const seen = new Set();
for (let calc of countdown(numbers)) {
if (calc.val === target) {
const repr = termToString(calc);
// If some number occurs twice in selection, solutions
// involving both numbers will be generated twice
if (!seen.has(repr)) console.log(repr);
seen.add(repr);
}
}
}
}