Files: 9964c6e5e9636cdc8b3f37a5cda9e898425b9583 / index.js
6184 bytesRaw
1 | |
2 | const NAME_REGEX = /^([a-z0-9\-.\/ ]+)$/i // eslint-disable-line |
3 | const MULTIPLIER_REGEX = /^(.*?)\s+x\s+(\d+)$/ |
4 | |
5 | function round(value) { |
6 | return Math.round(value * 100) / 100 |
7 | } |
8 | |
9 | module.exports = class Plainbudget { |
10 | |
11 | static computeSheet (sheet) { |
12 | const pb = new Plainbudget(sheet) |
13 | |
14 | return pb.compute() |
15 | } |
16 | |
17 | static computeSheets (sheets) { |
18 | const allGroups = [] |
19 | let allNamed = {} |
20 | |
21 | const instances = Object.keys(sheets) |
22 | .reduce((obj, s) => { |
23 | const pb = new Plainbudget(sheets[s]) |
24 | |
25 | pb.parse() |
26 | pb.calcNamed() |
27 | allGroups.splice(allGroups.length, 0, Object.assign({}, pb.groups)) |
28 | allNamed = Object.assign({}, allNamed, pb.named) |
29 | |
30 | return Object.assign({}, obj, { [s]: pb }) |
31 | }, {}) |
32 | |
33 | return Object.keys(instances) |
34 | .reduce((obj, key) => { |
35 | const i = instances[key] |
36 | |
37 | i.named = allNamed |
38 | i.padding = i.getPadding(allGroups) |
39 | i.calcFlows() |
40 | i.compute(false) |
41 | |
42 | return Object.assign({}, obj, { [key]: i.text }) |
43 | }, {}) |
44 | } |
45 | |
46 | constructor (text) { |
47 | this.padding = 3 |
48 | this.lines = [] |
49 | this.groups = [] |
50 | this.named = {} |
51 | this.text = text |
52 | } |
53 | |
54 | parseValue (line) { |
55 | let value = line.slice(1).match(/^\s+(\d+(.\d+)?)/) |
56 | |
57 | if (value) { |
58 | value = parseFloat(value[1]) |
59 | |
60 | if (isNaN(value)) { |
61 | value = '?' |
62 | } |
63 | } |
64 | |
65 | return value |
66 | } |
67 | |
68 | parseLine (line) { |
69 | const value = this.parseValue(line) |
70 | let label = line.slice(1).match(/\d+\s+(.+)/) |
71 | |
72 | label = label ? label[1] : line.slice(1).trim() |
73 | |
74 | return [line[0], value, label] |
75 | } |
76 | |
77 | parseMultiplier (label) { |
78 | const m = label.trim().match(MULTIPLIER_REGEX) |
79 | |
80 | if (m) { |
81 | return [m[1], m[2]] |
82 | } |
83 | } |
84 | |
85 | groupEquals (groupA, groupB) { |
86 | return ( |
87 | groupA[0] === groupB[0] && |
88 | groupA[1] === groupB[1] && |
89 | groupA[2] === groupB[2] |
90 | ) |
91 | } |
92 | |
93 | parse () { |
94 | this.named = {} |
95 | this.groups = [] |
96 | this.lines = this.text.split(/\n/) |
97 | |
98 | let group = null |
99 | |
100 | for (let i = 0; i < this.lines.length; i++) { |
101 | const line = this.lines[i].trim() |
102 | const op = line[0] |
103 | |
104 | if ('=+'.includes(op) && group === null) { |
105 | group = [this.parseLine(line)] |
106 | } else if ('-~+x'.includes(op)) { |
107 | group.push(this.parseLine(line)) |
108 | } else if (line.match(/^\s*$/) && group !== null && group.length > 1) { |
109 | this.groups.push(group) |
110 | group = null |
111 | } |
112 | } |
113 | |
114 | if (group !== null && !this.groupEquals(this.groups.slice(-1), group) && group.length > 1) { |
115 | this.groups.push(group) |
116 | } |
117 | } |
118 | |
119 | compute (calc = true) { |
120 | if (calc) { |
121 | this.parse() |
122 | this.calcNamed() |
123 | this.calcFlows() |
124 | } |
125 | |
126 | const padding = Math.max(this.padding, this.getPadding()) |
127 | const updated = [] |
128 | |
129 | for (let x = 0; x < this.groups.length; x++) { |
130 | const group = this.groups[x] |
131 | |
132 | for (let y = 0; y < group.length; y++) { |
133 | const op = group[y] |
134 | |
135 | updated.push(`${op[0]} ${op[1].toString().padStart(padding)} ${op[2]}\n`) |
136 | } |
137 | |
138 | updated.push('\n') |
139 | } |
140 | |
141 | this.text = `\n${updated.join('').trim()}\n` |
142 | |
143 | return this.text |
144 | } |
145 | |
146 | setNamed (label, value) { |
147 | const varMatch = label.match(NAME_REGEX) |
148 | |
149 | if (varMatch) { |
150 | label = varMatch[1] |
151 | this.named[label] = value |
152 | } |
153 | } |
154 | |
155 | getNamed (label) { |
156 | if (label.match(MULTIPLIER_REGEX)) { |
157 | label = label.split(/\s+x\s+/)[0] |
158 | } |
159 | |
160 | const varMatch = label.match(NAME_REGEX) |
161 | |
162 | if (varMatch && varMatch[1] in this.named) { |
163 | return [true, this.named[varMatch[1]]] |
164 | } |
165 | |
166 | return [false] |
167 | } |
168 | |
169 | calcNamed () { |
170 | this.calc(this.groups.reduce((arr, g, i) => { |
171 | if (g[0][0] === '=') { |
172 | return arr.concat([i]) |
173 | } |
174 | |
175 | return arr |
176 | }, [])) |
177 | } |
178 | |
179 | calcFlows () { |
180 | this.calc(this.groups.reduce((arr, g, i) => { |
181 | if (g[0][0] !== '=') { |
182 | return arr.concat([i]) |
183 | } |
184 | |
185 | return arr |
186 | }, [])) |
187 | } |
188 | |
189 | processCashflowEntry (op) { |
190 | const multiplier = this.parseMultiplier(op[2]) |
191 | let named |
192 | |
193 | if (multiplier) { |
194 | named = this.getNamed(multiplier[0], op[1]) |
195 | |
196 | if (named[0]) { |
197 | op[1] = named[1] * parseInt(multiplier[1]) |
198 | return op[1] |
199 | } |
200 | |
201 | return op[1] * parseInt(multiplier[1]) |
202 | } |
203 | |
204 | named = this.getNamed(op[2], op[1]) |
205 | |
206 | if (named[0]) { |
207 | op[1] = named[1] |
208 | } |
209 | |
210 | return op[1] |
211 | } |
212 | |
213 | calc (groupIndices) { |
214 | if (!groupIndices.length) { |
215 | return |
216 | } |
217 | |
218 | let value |
219 | |
220 | for (let g = 0; g < groupIndices.length; g++) { |
221 | const group = this.groups[groupIndices[g]] |
222 | |
223 | if ('='.includes(group[0][0])) { |
224 | if (group[0][2] === '') { |
225 | group[0][2] = '\n' |
226 | } |
227 | |
228 | value = 0 |
229 | const topOps = group.slice(1) |
230 | |
231 | for (let x = 0; x < topOps.length; x++) { |
232 | const topOp = topOps[x] |
233 | |
234 | if (topOp[1] === '?' || topOp[0] === 'x') { |
235 | continue |
236 | } |
237 | |
238 | const multiplier = this.parseMultiplier(topOp[2]) |
239 | |
240 | if (multiplier) { |
241 | value += topOp[1] * parseFloat(multiplier[1]) |
242 | } else { |
243 | value += topOp[1] |
244 | } |
245 | } |
246 | } else if (group[0][0] === '+') { |
247 | value = group[0][1] |
248 | const ops = group.slice(1) |
249 | |
250 | for (let y = 0; y < ops.length; y++) { |
251 | const op = ops[y] |
252 | |
253 | if (op[1] === '?' || op[0] === 'x') { |
254 | continue |
255 | } |
256 | |
257 | if (op[0] === '+') { |
258 | value += this.processCashflowEntry(op) |
259 | } else if ('-~'.includes(op[0])) { |
260 | value -= this.processCashflowEntry(op) |
261 | } |
262 | } |
263 | } |
264 | |
265 | value = round(value) |
266 | |
267 | if (group[0][0] === '=') { |
268 | group[0][1] = value |
269 | this.setNamed(group[0][2], value) |
270 | } else { |
271 | group.push(['=', value, '']) |
272 | } |
273 | } |
274 | } |
275 | |
276 | getPadding (groups) { |
277 | groups = groups || this.groups |
278 | |
279 | let p = 3 |
280 | |
281 | for (let x = 0; x < groups.length; x++) { |
282 | const group = groups[x] |
283 | |
284 | for (let y = 0; y < group.length; y++) { |
285 | if (group[y][1] !== null) { |
286 | const nlen = group[y][1].toString().length |
287 | |
288 | if (nlen > (p + 1)) { |
289 | p = nlen + 1 |
290 | } |
291 | } |
292 | } |
293 | } |
294 | |
295 | return p + 1 |
296 | } |
297 | } |
298 |
Built with git-ssb-web