@sdeverywhere/compile 0.7.29 → 0.7.31
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/package.json +3 -3
- package/src/_shared/helpers.js +7 -18
- package/src/_shared/xlsx.js +502 -0
- package/src/generate/direct-data-helpers.js +2 -3
- package/src/generate/gen-direct-const.js +2 -3
- package/src/generate/gen-expr.js +88 -0
- package/src/generate/gen-lookup-from-direct.js +4 -4
- package/src/model/read-equations.js +8 -0
- package/src/model/read-subscripts.js +2 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdeverywhere/compile",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.31",
|
|
4
4
|
"description": "The core Vensim to C compiler for the SDEverywhere tool suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"@sdeverywhere/parse": "^0.1.4",
|
|
9
9
|
"byline": "^5.0.0",
|
|
10
10
|
"csv-parse": "^5.3.3",
|
|
11
|
+
"fflate": "^0.8.3",
|
|
11
12
|
"ramda": "^0.27.0",
|
|
12
|
-
"strip-bom": "^5.0.0"
|
|
13
|
-
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
|
|
13
|
+
"strip-bom": "^5.0.0"
|
|
14
14
|
},
|
|
15
15
|
"author": "Climate Interactive",
|
|
16
16
|
"license": "MIT",
|
package/src/_shared/helpers.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import * as fs from 'node:fs'
|
|
2
1
|
import util from 'util'
|
|
3
2
|
import { parse as parseCsv } from 'csv-parse/sync'
|
|
4
3
|
import * as R from 'ramda'
|
|
5
|
-
|
|
4
|
+
|
|
6
5
|
import B from './bufx.js'
|
|
6
|
+
import { readXlsx as readXlsxFile, resetXlsxCache } from './xlsx.js'
|
|
7
7
|
|
|
8
8
|
import { canonicalId, canonicalVarId } from '@sdeverywhere/parse'
|
|
9
9
|
|
|
@@ -24,12 +24,6 @@ let nextLevelVarSeq = 1
|
|
|
24
24
|
let nextAuxVarSeq = 1
|
|
25
25
|
// parsed csv data cache
|
|
26
26
|
let csvData = new Map()
|
|
27
|
-
// parsed xlsx data cache
|
|
28
|
-
let xlsxData = new Map()
|
|
29
|
-
|
|
30
|
-
// Newer versions of the xlsx package require manually setting the `fs` instance
|
|
31
|
-
// before using the `XLSX.readFile` function
|
|
32
|
-
XLSX.set_fs(fs)
|
|
33
27
|
|
|
34
28
|
// XXX: This is needed for tests due to sequence numbers being in module-level storage
|
|
35
29
|
export function resetHelperState() {
|
|
@@ -40,7 +34,7 @@ export function resetHelperState() {
|
|
|
40
34
|
nextLevelVarSeq = 1
|
|
41
35
|
nextAuxVarSeq = 1
|
|
42
36
|
csvData.clear()
|
|
43
|
-
|
|
37
|
+
resetXlsxCache()
|
|
44
38
|
}
|
|
45
39
|
|
|
46
40
|
export let canonicalName = name => {
|
|
@@ -203,15 +197,10 @@ export let isIterable = obj => {
|
|
|
203
197
|
}
|
|
204
198
|
// Command helpers
|
|
205
199
|
export let readXlsx = pathname => {
|
|
206
|
-
// Read the XLSX file at the pathname and parse it.
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
if (!xlsx) {
|
|
211
|
-
xlsx = XLSX.readFile(pathname, { cellDates: true })
|
|
212
|
-
xlsxData.set(pathname, xlsx)
|
|
213
|
-
}
|
|
214
|
-
return xlsx
|
|
200
|
+
// Read the XLSX file at the pathname and parse it. Return a workbook
|
|
201
|
+
// object ({ SheetNames, Sheets }) that can be used to access the data.
|
|
202
|
+
// The underlying reader caches parsed files by path.
|
|
203
|
+
return readXlsxFile(pathname)
|
|
215
204
|
}
|
|
216
205
|
export let readCsv = (pathname, delimiter = ',') => {
|
|
217
206
|
// Read the CSV file at the pathname and parse it with the given delimiter.
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
// Copyright (c) 2026 Climate Interactive / New Venture Fund
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
|
|
5
|
+
import { strFromU8, unzipSync } from 'fflate'
|
|
6
|
+
|
|
7
|
+
//
|
|
8
|
+
// Minimal xlsx reader and cell-address utilities used by the `GET DIRECT ...`
|
|
9
|
+
// code paths in the compile pipeline. We read numeric cell values only; strings
|
|
10
|
+
// are surfaced where present but are not the focus of this module.
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
const A_UPPER = 65 // 'A'
|
|
14
|
+
const A_LOWER = 97 // 'a'
|
|
15
|
+
|
|
16
|
+
// Workbook cache, mirroring the previous SheetJS readXlsx behavior in
|
|
17
|
+
// helpers.js. Each xlsx file is parsed at most once per process.
|
|
18
|
+
const workbookCache = new Map()
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reset the workbook cache. Intended for tests that load the same path with
|
|
22
|
+
* different contents across runs.
|
|
23
|
+
*/
|
|
24
|
+
export function resetXlsxCache() {
|
|
25
|
+
workbookCache.clear()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the xlsx file at the given path and return a workbook shaped like the
|
|
30
|
+
* SheetJS `WorkBook`:
|
|
31
|
+
*
|
|
32
|
+
* ```
|
|
33
|
+
* { SheetNames: string[],
|
|
34
|
+
* Sheets: { [name]: { [cellRef]: { v }, '!ref': 'A1:Z99' } } }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Sheets are materialized lazily — the first time a sheet name is read from
|
|
38
|
+
* `Sheets`, its XML is parsed; subsequent reads of the same sheet return the
|
|
39
|
+
* cached map. Workbooks are also cached by path.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} pathname The absolute path to the xlsx file.
|
|
42
|
+
* @returns The parsed workbook.
|
|
43
|
+
*/
|
|
44
|
+
export function readXlsx(pathname) {
|
|
45
|
+
// Return the cached workbook if we've already parsed this file
|
|
46
|
+
const cached = workbookCache.get(pathname)
|
|
47
|
+
if (cached) {
|
|
48
|
+
return cached
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Decompress the file, keeping only the entries we actually read from
|
|
52
|
+
const buf = readFileSync(pathname)
|
|
53
|
+
const unzipped = unzipSync(buf, {
|
|
54
|
+
filter: file => {
|
|
55
|
+
const n = file.name
|
|
56
|
+
return (
|
|
57
|
+
n === 'xl/workbook.xml' ||
|
|
58
|
+
n === 'xl/sharedStrings.xml' ||
|
|
59
|
+
n === 'xl/_rels/workbook.xml.rels' ||
|
|
60
|
+
(n.startsWith('xl/worksheets/sheet') && n.endsWith('.xml'))
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Pull out the workbook, rels, and (optional) sharedStrings parts as text
|
|
66
|
+
const wbBytes = unzipped['xl/workbook.xml']
|
|
67
|
+
const relsBytes = unzipped['xl/_rels/workbook.xml.rels']
|
|
68
|
+
if (!wbBytes || !relsBytes) {
|
|
69
|
+
throw new Error(`Failed to read xlsx file (missing workbook or rels): ${pathname}`)
|
|
70
|
+
}
|
|
71
|
+
const wbXml = strFromU8(wbBytes)
|
|
72
|
+
const relsXml = strFromU8(relsBytes)
|
|
73
|
+
const ssBytes = unzipped['xl/sharedStrings.xml']
|
|
74
|
+
|
|
75
|
+
// Build the sheet definitions
|
|
76
|
+
const sheetDefs = parseWorkbookXml(wbXml)
|
|
77
|
+
|
|
78
|
+
// Build the rid-to-target map
|
|
79
|
+
const rels = parseWorkbookRels(relsXml)
|
|
80
|
+
|
|
81
|
+
// Build the shared-string table
|
|
82
|
+
const sharedStrings = ssBytes ? parseSharedStrings(strFromU8(ssBytes)) : []
|
|
83
|
+
|
|
84
|
+
// Resolve each sheet's rid to its worksheet XML bytes in the zip
|
|
85
|
+
const sheetNames = []
|
|
86
|
+
const sheetXmls = Object.create(null)
|
|
87
|
+
for (const { name, rid } of sheetDefs) {
|
|
88
|
+
let target = rels[rid]
|
|
89
|
+
if (!target) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
// Targets are workbook-relative; normalize to the zip entry path
|
|
93
|
+
target = target.startsWith('/') ? target.slice(1) : 'xl/' + target
|
|
94
|
+
const bytes = unzipped[target]
|
|
95
|
+
if (!bytes) {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
sheetNames.push(name)
|
|
99
|
+
sheetXmls[name] = bytes
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Lazy materialization: parse a sheet only when first accessed
|
|
103
|
+
const parsedSheets = Object.create(null)
|
|
104
|
+
const sheetsProxy = new Proxy(
|
|
105
|
+
{},
|
|
106
|
+
{
|
|
107
|
+
get(_, name) {
|
|
108
|
+
if (typeof name !== 'string') {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
if (parsedSheets[name]) {
|
|
112
|
+
return parsedSheets[name]
|
|
113
|
+
}
|
|
114
|
+
const bytes = sheetXmls[name]
|
|
115
|
+
if (!bytes) {
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
const parsed = parseSheetXml(strFromU8(bytes), sharedStrings)
|
|
119
|
+
parsedSheets[name] = parsed
|
|
120
|
+
return parsed
|
|
121
|
+
},
|
|
122
|
+
has(_, name) {
|
|
123
|
+
return typeof name === 'string' && name in sheetXmls
|
|
124
|
+
},
|
|
125
|
+
ownKeys() {
|
|
126
|
+
return sheetNames.slice()
|
|
127
|
+
},
|
|
128
|
+
getOwnPropertyDescriptor(_, name) {
|
|
129
|
+
if (typeof name !== 'string' || !(name in sheetXmls)) {
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
const bytes = sheetXmls[name]
|
|
133
|
+
if (!parsedSheets[name]) {
|
|
134
|
+
parsedSheets[name] = parseSheetXml(strFromU8(bytes), sharedStrings)
|
|
135
|
+
}
|
|
136
|
+
return { enumerable: true, configurable: true, value: parsedSheets[name], writable: false }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// Cache the workbook by path so subsequent reads are free
|
|
142
|
+
const workbook = { SheetNames: sheetNames, Sheets: sheetsProxy }
|
|
143
|
+
workbookCache.set(pathname, workbook)
|
|
144
|
+
return workbook
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//
|
|
148
|
+
// Cell address utilities
|
|
149
|
+
//
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Decode an A1-style cell ref to a zero-indexed `{c, r}`.
|
|
153
|
+
*
|
|
154
|
+
* Returns `{c: -1, r: -1}` for invalid input, matching the behavior of
|
|
155
|
+
* `XLSX.utils.decode_cell` from SheetJS.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} ref The cell reference (e.g. 'A1', 'AZ100').
|
|
158
|
+
* @returns The zero-indexed column and row.
|
|
159
|
+
*/
|
|
160
|
+
export function decodeCell(ref) {
|
|
161
|
+
// Walk the leading run of letters, accumulating the 1-based column index
|
|
162
|
+
// in bijective base 26 (A=1, B=2, ..., Z=26, AA=27, AB=28, ...)
|
|
163
|
+
let c = 0
|
|
164
|
+
let i = 0
|
|
165
|
+
const len = ref.length
|
|
166
|
+
while (i < len) {
|
|
167
|
+
const code = ref.charCodeAt(i)
|
|
168
|
+
if (code >= 65 && code <= 90) {
|
|
169
|
+
c = c * 26 + (code - A_UPPER + 1)
|
|
170
|
+
} else if (code >= 97 && code <= 122) {
|
|
171
|
+
c = c * 26 + (code - A_LOWER + 1)
|
|
172
|
+
} else {
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
i++
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Reject input that didn't start with at least one letter
|
|
179
|
+
if (i === 0) {
|
|
180
|
+
return { c: -1, r: -1 }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Parse the trailing row digits; rows are 1-based, so reject 0/non-numeric
|
|
184
|
+
const r = parseInt(ref.slice(i), 10)
|
|
185
|
+
if (!Number.isFinite(r) || r < 1) {
|
|
186
|
+
return { c: -1, r: -1 }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Convert column and row to the zero-indexed form callers expect
|
|
190
|
+
return { c: c - 1, r: r - 1 }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Encode a zero-indexed `{c, r}` to an A1-style cell ref.
|
|
195
|
+
*
|
|
196
|
+
* @param {{c: number, r: number}} addr The zero-indexed column and row.
|
|
197
|
+
* @returns The A1-style cell reference (e.g. 'B7').
|
|
198
|
+
*/
|
|
199
|
+
export function encodeCell({ c, r }) {
|
|
200
|
+
// Convert the 1-based column index to bijective base-26 letters, building
|
|
201
|
+
// the string right-to-left (one letter per iteration, least-significant first)
|
|
202
|
+
let col = ''
|
|
203
|
+
let n = c + 1
|
|
204
|
+
while (n > 0) {
|
|
205
|
+
const rem = (n - 1) % 26
|
|
206
|
+
col = String.fromCharCode(A_UPPER + rem) + col
|
|
207
|
+
n = Math.floor((n - 1) / 26)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Rows in A1 notation are 1-based
|
|
211
|
+
return col + (r + 1)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Decode a column ref (e.g. 'AB') to a zero-indexed column number.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} ref The column reference.
|
|
218
|
+
* @returns The zero-indexed column number, or -1 if the input is invalid.
|
|
219
|
+
*/
|
|
220
|
+
export function decodeCol(ref) {
|
|
221
|
+
// Empty input has no valid column
|
|
222
|
+
if (ref.length === 0) {
|
|
223
|
+
return -1
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Same bijective base-26 walk as decodeCell, but every character must be a
|
|
227
|
+
// letter — anything else (including a row digit) is rejected
|
|
228
|
+
let c = 0
|
|
229
|
+
for (let i = 0; i < ref.length; i++) {
|
|
230
|
+
const code = ref.charCodeAt(i)
|
|
231
|
+
if (code >= 65 && code <= 90) {
|
|
232
|
+
c = c * 26 + (code - A_UPPER + 1)
|
|
233
|
+
} else if (code >= 97 && code <= 122) {
|
|
234
|
+
c = c * 26 + (code - A_LOWER + 1)
|
|
235
|
+
} else {
|
|
236
|
+
return -1
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Convert from 1-based to zero-indexed
|
|
241
|
+
return c - 1
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Decode a row ref (e.g. '13') to a zero-indexed row number.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} ref The row reference.
|
|
248
|
+
* @returns The zero-indexed row number, or -1 if the input is invalid.
|
|
249
|
+
*/
|
|
250
|
+
export function decodeRow(ref) {
|
|
251
|
+
// Parse the 1-based row number and convert to zero-indexed; -1 on invalid input
|
|
252
|
+
const r = parseInt(ref, 10)
|
|
253
|
+
return Number.isFinite(r) && r >= 1 ? r - 1 : -1
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
//
|
|
257
|
+
// XML helpers
|
|
258
|
+
//
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Pull one attribute value out of a tag's attribute list. Cheaper than a full
|
|
262
|
+
* attribute parser when we only need a few specific keys.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} attrs The portion of a start tag containing the attributes.
|
|
265
|
+
* @param {string} name The attribute name to look up.
|
|
266
|
+
* @returns The attribute value, or undefined if the attribute is not present.
|
|
267
|
+
*/
|
|
268
|
+
function getAttr(attrs, name) {
|
|
269
|
+
// Look for `name="` — every attribute we care about uses double quotes
|
|
270
|
+
const i = attrs.indexOf(name + '="')
|
|
271
|
+
if (i < 0) {
|
|
272
|
+
return undefined
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Slice out everything between the opening and closing quote
|
|
276
|
+
const start = i + name.length + 2
|
|
277
|
+
const end = attrs.indexOf('"', start)
|
|
278
|
+
return end < 0 ? undefined : attrs.slice(start, end)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Decode the standard XML entities (`<`, `>`, `&`, `"`,
|
|
283
|
+
* `'`) along with numeric character references (`&#NN;` and `&#xNN;`)
|
|
284
|
+
* in the given text. Returns the input unchanged when no entities are present.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} s The raw text from an XML element body or attribute.
|
|
287
|
+
* @returns The decoded string.
|
|
288
|
+
*/
|
|
289
|
+
function decodeXmlText(s) {
|
|
290
|
+
// Fast path: most cell text contains no entities, so skip the regex chain
|
|
291
|
+
if (s.indexOf('&') === -1) {
|
|
292
|
+
return s
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Decode the named entities, then decimal and hex numeric refs, and finally
|
|
296
|
+
// `&` — leaving `&` last avoids accidentally producing `<` etc.
|
|
297
|
+
// from a literal `&lt;` in the source
|
|
298
|
+
return s
|
|
299
|
+
.replace(/</g, '<')
|
|
300
|
+
.replace(/>/g, '>')
|
|
301
|
+
.replace(/"/g, '"')
|
|
302
|
+
.replace(/'/g, "'")
|
|
303
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
|
|
304
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)))
|
|
305
|
+
.replace(/&/g, '&')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
//
|
|
309
|
+
// Parsers
|
|
310
|
+
//
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Parse the contents of `xl/sharedStrings.xml` into a positional array of
|
|
314
|
+
* decoded strings. A shared string may contain rich-text runs
|
|
315
|
+
* (`<si><r><t>foo</t></r><r><t>bar</t></r></si>`); concatenate all `<t>`
|
|
316
|
+
* elements within each `<si>` so the result comes back as a single string.
|
|
317
|
+
*
|
|
318
|
+
* @param {string} xml The contents of the `xl/sharedStrings.xml` file.
|
|
319
|
+
* @returns An array indexed by shared-string position.
|
|
320
|
+
*/
|
|
321
|
+
function parseSharedStrings(xml) {
|
|
322
|
+
// Outer regex finds each <si> entry; inner regex finds <t> runs within it.
|
|
323
|
+
// The two are stateful (global) regexes, so reset the inner one per <si>.
|
|
324
|
+
const strings = []
|
|
325
|
+
const siRe = /<si\b[^>]*>([\s\S]*?)<\/si>/g
|
|
326
|
+
const tRe = /<t\b[^>]*>([\s\S]*?)<\/t>/g
|
|
327
|
+
let m
|
|
328
|
+
while ((m = siRe.exec(xml)) !== null) {
|
|
329
|
+
// Concatenate every <t> chunk inside this <si> — rich-text runs collapse
|
|
330
|
+
// into a single plain string
|
|
331
|
+
const inner = m[1]
|
|
332
|
+
let s = ''
|
|
333
|
+
let tm
|
|
334
|
+
tRe.lastIndex = 0
|
|
335
|
+
while ((tm = tRe.exec(inner)) !== null) {
|
|
336
|
+
s += decodeXmlText(tm[1])
|
|
337
|
+
}
|
|
338
|
+
strings.push(s)
|
|
339
|
+
}
|
|
340
|
+
return strings
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Parse the contents of `xl/workbook.xml` into the list of sheet definitions,
|
|
345
|
+
* each with the user-visible sheet name and the relationship id that maps to
|
|
346
|
+
* the sheet's XML part. Attribute spans can contain `/` (in URLs), so the
|
|
347
|
+
* regex matches up to the self-closing `/>` non-greedily rather than excluding
|
|
348
|
+
* `/` from the attribute span.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} xml The contents of the `xl/workbook.xml` file.
|
|
351
|
+
* @returns An array of `{ name, rid }` objects in workbook order.
|
|
352
|
+
*/
|
|
353
|
+
function parseWorkbookXml(xml) {
|
|
354
|
+
// Iterate every <sheet .../> element in workbook order
|
|
355
|
+
const sheets = []
|
|
356
|
+
const re = /<sheet\b([\s\S]*?)\/>/g
|
|
357
|
+
let m
|
|
358
|
+
while ((m = re.exec(xml)) !== null) {
|
|
359
|
+
// Each sheet is identified by its user-visible name and its rid pointer;
|
|
360
|
+
// the rid attribute is conventionally lowercase but accept both forms
|
|
361
|
+
const attrs = m[1]
|
|
362
|
+
const name = getAttr(attrs, 'name')
|
|
363
|
+
const rid = getAttr(attrs, 'r:id') ?? getAttr(attrs, 'r:Id')
|
|
364
|
+
if (name && rid) {
|
|
365
|
+
sheets.push({ name, rid })
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return sheets
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Parse the contents of `xl/_rels/workbook.xml.rels` into a map from
|
|
373
|
+
* relationship id to its target path (the part within the xlsx zip).
|
|
374
|
+
*
|
|
375
|
+
* @param {string} xml The contents of the `xl/_rels/workbook.xml.rels` file.
|
|
376
|
+
* @returns An object mapping relationship id to target path.
|
|
377
|
+
*/
|
|
378
|
+
function parseWorkbookRels(xml) {
|
|
379
|
+
// Iterate every <Relationship .../> element
|
|
380
|
+
const rels = Object.create(null)
|
|
381
|
+
const re = /<Relationship\b([\s\S]*?)\/>/g
|
|
382
|
+
let m
|
|
383
|
+
while ((m = re.exec(xml)) !== null) {
|
|
384
|
+
// Record the Id -> Target mapping; ignore the Type and other attributes
|
|
385
|
+
const attrs = m[1]
|
|
386
|
+
const id = getAttr(attrs, 'Id')
|
|
387
|
+
const target = getAttr(attrs, 'Target')
|
|
388
|
+
if (id && target) {
|
|
389
|
+
rels[id] = target
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return rels
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Scan a worksheet's XML and build a sparse cell map shaped like the SheetJS
|
|
397
|
+
* worksheet object: `{ [cellRef]: { v }, '!ref': 'A1:Z99' }`. Skips empty
|
|
398
|
+
* cells, error cells (`t='e'`), and numeric cells whose cached `<v>` value
|
|
399
|
+
* is missing.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} xml The contents of a `xl/worksheets/sheet*.xml` file.
|
|
402
|
+
* @param {string[]} sharedStrings The shared-string table for resolving `t='s'` cells.
|
|
403
|
+
* @returns The sparse cell map, including a `!ref` key if any cells were read.
|
|
404
|
+
*/
|
|
405
|
+
function parseSheetXml(xml, sharedStrings) {
|
|
406
|
+
const cells = Object.create(null)
|
|
407
|
+
|
|
408
|
+
// Match each <c .../> or <c ...>...</c> block. The attribute span is
|
|
409
|
+
// non-greedy so self-closing cells (e.g. <c r="I4" s="1"/>) don't accidentally
|
|
410
|
+
// swallow following cells.
|
|
411
|
+
const cRe = /<c\b([^>]*?)(\/>|>([\s\S]*?)<\/c>)/g
|
|
412
|
+
let maxRow = -1
|
|
413
|
+
let maxCol = -1
|
|
414
|
+
let m
|
|
415
|
+
while ((m = cRe.exec(xml)) !== null) {
|
|
416
|
+
const attrs = m[1]
|
|
417
|
+
const ref = getAttr(attrs, 'r')
|
|
418
|
+
if (!ref) {
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
if (m[2] === '/>') {
|
|
422
|
+
// empty cell
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
const body = m[3]
|
|
426
|
+
if (!body) {
|
|
427
|
+
continue
|
|
428
|
+
}
|
|
429
|
+
const t = getAttr(attrs, 't')
|
|
430
|
+
|
|
431
|
+
let value
|
|
432
|
+
if (t === 's') {
|
|
433
|
+
// Shared string: <v>N</v> where N indexes sharedStrings
|
|
434
|
+
const vStart = body.indexOf('<v>')
|
|
435
|
+
if (vStart < 0) {
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
const vEnd = body.indexOf('</v>', vStart + 3)
|
|
439
|
+
const idx = parseInt(body.slice(vStart + 3, vEnd), 10)
|
|
440
|
+
value = sharedStrings[idx]
|
|
441
|
+
} else if (t === 'inlineStr') {
|
|
442
|
+
// Inline string: <is><t>...</t></is>
|
|
443
|
+
const tStart = body.indexOf('<t')
|
|
444
|
+
if (tStart < 0) {
|
|
445
|
+
continue
|
|
446
|
+
}
|
|
447
|
+
const tOpenEnd = body.indexOf('>', tStart)
|
|
448
|
+
const tEnd = body.indexOf('</t>', tOpenEnd)
|
|
449
|
+
value = decodeXmlText(body.slice(tOpenEnd + 1, tEnd))
|
|
450
|
+
} else if (t === 'str') {
|
|
451
|
+
// Formula result as string: <v>...</v>
|
|
452
|
+
const vStart = body.indexOf('<v>')
|
|
453
|
+
if (vStart < 0) {
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
const vEnd = body.indexOf('</v>', vStart + 3)
|
|
457
|
+
value = decodeXmlText(body.slice(vStart + 3, vEnd))
|
|
458
|
+
} else if (t === 'b') {
|
|
459
|
+
// Boolean: <v>0</v> or <v>1</v>
|
|
460
|
+
const vStart = body.indexOf('<v>')
|
|
461
|
+
if (vStart < 0) {
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
value = body.charCodeAt(vStart + 3) === 49 // '1'
|
|
465
|
+
} else if (t === 'e') {
|
|
466
|
+
// Error cell, skip
|
|
467
|
+
continue
|
|
468
|
+
} else {
|
|
469
|
+
// Numeric (t === 'n' or absent). Skip any <f> formula tag and read the
|
|
470
|
+
// cached <v> value. If <v> is missing (e.g. an uncalculated formula),
|
|
471
|
+
// skip the cell so the caller's missing-cell handling kicks in.
|
|
472
|
+
const vStart = body.indexOf('<v>')
|
|
473
|
+
if (vStart < 0) {
|
|
474
|
+
continue
|
|
475
|
+
}
|
|
476
|
+
const vEnd = body.indexOf('</v>', vStart + 3)
|
|
477
|
+
const num = +body.slice(vStart + 3, vEnd)
|
|
478
|
+
if (Number.isNaN(num)) {
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
value = num
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Store the cell under its A1 ref, matching the SheetJS sheet shape
|
|
485
|
+
cells[ref] = { v: value }
|
|
486
|
+
|
|
487
|
+
// Track the bounding row/col so we can synthesize the !ref range below
|
|
488
|
+
const addr = decodeCell(ref)
|
|
489
|
+
if (addr.r > maxRow) {
|
|
490
|
+
maxRow = addr.r
|
|
491
|
+
}
|
|
492
|
+
if (addr.c > maxCol) {
|
|
493
|
+
maxCol = addr.c
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Expose the sheet's bounding range as !ref when any cells were read
|
|
498
|
+
if (maxRow >= 0) {
|
|
499
|
+
cells['!ref'] = `A1:${encodeCell({ c: maxCol, r: maxRow })}`
|
|
500
|
+
}
|
|
501
|
+
return cells
|
|
502
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
|
|
3
|
-
import XLSX from 'xlsx'
|
|
4
|
-
|
|
5
3
|
import { cdbl, readCsv, readXlsx } from '../_shared/helpers.js'
|
|
4
|
+
import { encodeCell } from '../_shared/xlsx.js'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Return a `getCellValue` function that reads the CSV or XLS[X] content.
|
|
@@ -47,7 +46,7 @@ function handleExcelWorkbook(fileOrTag, workbook, tab, dataKind, dataSource) {
|
|
|
47
46
|
let sheet = workbook.Sheets[tab]
|
|
48
47
|
if (sheet) {
|
|
49
48
|
return (c, r) => {
|
|
50
|
-
let cell = sheet[
|
|
49
|
+
let cell = sheet[encodeCell({ c, r })]
|
|
51
50
|
if (cell == null || cell.v === '') {
|
|
52
51
|
return null
|
|
53
52
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import XLSX from 'xlsx'
|
|
2
|
-
|
|
3
1
|
import { cartesianProductOf } from '../_shared/helpers.js'
|
|
4
2
|
import { indexInSepDim, isDimension, sub } from '../_shared/subscript.js'
|
|
3
|
+
import { decodeCell } from '../_shared/xlsx.js'
|
|
5
4
|
|
|
6
5
|
import { handleExcelOrCsvFile } from './direct-data-helpers.js'
|
|
7
6
|
|
|
@@ -73,7 +72,7 @@ export function generateDirectConstInit(variable, directData, modelDir) {
|
|
|
73
72
|
// Read tabular data into an indexed variable for each cell.
|
|
74
73
|
let numericSubscripts = lhsIndexSubscripts.map(idx => idx.map(s => sub(s).value))
|
|
75
74
|
let lhsSubscripts = numericSubscripts.map(s => s.reduce((a, v) => a.concat(`[${v}]`), ''))
|
|
76
|
-
let dataAddress =
|
|
75
|
+
let dataAddress = decodeCell(startCell.toUpperCase())
|
|
77
76
|
let startCol = dataAddress.c
|
|
78
77
|
let startRow = dataAddress.r
|
|
79
78
|
if (startCol < 0 || startRow < 0) {
|
package/src/generate/gen-expr.js
CHANGED
|
@@ -387,6 +387,12 @@ function generateFunctionCall(callExpr, ctx) {
|
|
|
387
387
|
}
|
|
388
388
|
return generateAllocateAvailableCall(callExpr, ctx)
|
|
389
389
|
|
|
390
|
+
case '_ALLOCATE_BY_PRIORITY':
|
|
391
|
+
if (ctx.outFormat === 'js') {
|
|
392
|
+
throw new Error(`${callExpr.fnName} function not yet implemented for JS code gen`)
|
|
393
|
+
}
|
|
394
|
+
return generateAllocateByPriorityCall(callExpr, ctx)
|
|
395
|
+
|
|
390
396
|
case '_ELMCOUNT':
|
|
391
397
|
case '_SIZE': {
|
|
392
398
|
// Emit the size of the dimension in place of the dimension name. Note that Vensim uses
|
|
@@ -944,6 +950,88 @@ function generateAllocateAvailableCall(callExpr, ctx) {
|
|
|
944
950
|
return `${tmpVarId}[${allocDimId}[${allocLoopIndexVar}]]`
|
|
945
951
|
}
|
|
946
952
|
|
|
953
|
+
/**
|
|
954
|
+
* Generate C/JS code for an `ALLOCATE BY PRIORITY` function call.
|
|
955
|
+
*
|
|
956
|
+
* @param {*} callExpr The function call expression from the parsed model.
|
|
957
|
+
* @param {GenExprContext} ctx The context used when generating code for the expression.
|
|
958
|
+
* @return {string} The generated C/JS code.
|
|
959
|
+
*/
|
|
960
|
+
function generateAllocateByPriorityCall(callExpr, ctx) {
|
|
961
|
+
function validateArg(index, name) {
|
|
962
|
+
const arg = callExpr.args[index]
|
|
963
|
+
if (arg.kind === 'variable-ref') {
|
|
964
|
+
return arg
|
|
965
|
+
} else {
|
|
966
|
+
throw new Error(`ALLOCATE BY PRIORITY argument '${name}' must be a variable reference`)
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Given a C/JS variable reference string (e.g., '_var[i][j]'), return that
|
|
971
|
+
// string without the last N array index parts
|
|
972
|
+
function cVarRefWithoutLastIndices(arg, count) {
|
|
973
|
+
const varRef = ctx.cVarRef(arg)
|
|
974
|
+
const origIndexParts = Model.splitRefId(varRef).subscripts
|
|
975
|
+
if (origIndexParts < count) {
|
|
976
|
+
throw new Error(`ALLOCATE BY PRIORITY argument '${arg}' should have at least ${count} subscripts`)
|
|
977
|
+
}
|
|
978
|
+
const newIndexParts = origIndexParts.slice(0, -count)
|
|
979
|
+
if (newIndexParts.length > 0) {
|
|
980
|
+
return `${arg.varId}${newIndexParts.map(x => `[${x}]`).join('')}`
|
|
981
|
+
} else {
|
|
982
|
+
return arg.varId
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Process the request argument. Only include subscripts up until the last one;
|
|
987
|
+
// the implementation function will iterate over the requesters array.
|
|
988
|
+
const reqArg = validateArg(0, 'req')
|
|
989
|
+
const reqRef = cVarRefWithoutLastIndices(reqArg, 1)
|
|
990
|
+
|
|
991
|
+
// Process the priority argument. Only include subscripts up until the
|
|
992
|
+
// last one; the implementation function will iterate over the priorities
|
|
993
|
+
// array.
|
|
994
|
+
const priorityArg = validateArg(1, 'priority')
|
|
995
|
+
const priorityRef = cVarRefWithoutLastIndices(priorityArg, 1)
|
|
996
|
+
|
|
997
|
+
// Process the size argument
|
|
998
|
+
const sizeArg = generateExpr(callExpr.args[2], ctx)
|
|
999
|
+
|
|
1000
|
+
// Process the width argument
|
|
1001
|
+
const widthArg = generateExpr(callExpr.args[3], ctx)
|
|
1002
|
+
|
|
1003
|
+
// Process the supply argument
|
|
1004
|
+
const supplyArg = generateExpr(callExpr.args[4], ctx)
|
|
1005
|
+
|
|
1006
|
+
// The `ALLOCATE BY PRIORITY` function iterates over the last subscript in its first
|
|
1007
|
+
// argument, allocating the available quantity according to the priority values given
|
|
1008
|
+
// in the second argument. The `readEquation` process will have already verified that
|
|
1009
|
+
// the last dimension of both arguments matches the last dimension of the LHS.
|
|
1010
|
+
const allocDimId = reqArg.subscriptRefs[reqArg.subscriptRefs.length - 1].subId
|
|
1011
|
+
const allocLoopIndexVar = ctx.loopIndexVars.index(allocDimId)
|
|
1012
|
+
|
|
1013
|
+
// Generate the code that is emitted before the entire block (before any loops are opened)
|
|
1014
|
+
const tmpVarId = newTmpVarName()
|
|
1015
|
+
const numRequesters = sub(allocDimId).size
|
|
1016
|
+
switch (ctx.outFormat) {
|
|
1017
|
+
case 'c':
|
|
1018
|
+
ctx.emitPreInnerLoop(
|
|
1019
|
+
` double* ${tmpVarId} = _ALLOCATE_BY_PRIORITY(${reqRef}, ${priorityRef}, ${sizeArg}, ${widthArg}, ${supplyArg}, ${numRequesters});`
|
|
1020
|
+
)
|
|
1021
|
+
break
|
|
1022
|
+
case 'js':
|
|
1023
|
+
ctx.emitPreInnerLoop(
|
|
1024
|
+
` let ${tmpVarId} = fns.ALLOCATE_BY_PRIORITY(${reqRef}, ${priorityRef}, ${sizeArg}, ${widthArg}, ${supplyArg}, ${numRequesters});`
|
|
1025
|
+
)
|
|
1026
|
+
break
|
|
1027
|
+
default:
|
|
1028
|
+
throw new Error(`Unhandled output format '${ctx.outFormat}'`)
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Generate the RHS expression used in the inner loop
|
|
1032
|
+
return `${tmpVarId}[${allocDimId}[${allocLoopIndexVar}]]`
|
|
1033
|
+
}
|
|
1034
|
+
|
|
947
1035
|
/**
|
|
948
1036
|
* Recursively traverse the given expression and call the function when visiting a variable ref.
|
|
949
1037
|
*
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as R from 'ramda'
|
|
2
|
-
import XLSX from 'xlsx'
|
|
3
2
|
|
|
4
3
|
import { listConcat } from '../_shared/helpers.js'
|
|
5
4
|
import { sub } from '../_shared/subscript.js'
|
|
5
|
+
import { decodeCell, decodeCol, decodeRow } from '../_shared/xlsx.js'
|
|
6
6
|
|
|
7
7
|
import { handleExcelOrCsvFile } from './direct-data-helpers.js'
|
|
8
8
|
|
|
@@ -61,7 +61,7 @@ function generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell,
|
|
|
61
61
|
// The cell(c,r) function wraps data access by column and row.
|
|
62
62
|
let lookupData = ''
|
|
63
63
|
let lookupSize = 0
|
|
64
|
-
let dataAddress =
|
|
64
|
+
let dataAddress = decodeCell(startCell.toUpperCase())
|
|
65
65
|
let dataCol = dataAddress.c
|
|
66
66
|
let dataRow = dataAddress.r
|
|
67
67
|
if (dataCol < 0 || dataRow < 0) {
|
|
@@ -71,7 +71,7 @@ function generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell,
|
|
|
71
71
|
let timeCol, timeRow, nextCell
|
|
72
72
|
if (isNaN(parseInt(timeRowOrCol))) {
|
|
73
73
|
// Time values are in a column.
|
|
74
|
-
timeCol =
|
|
74
|
+
timeCol = decodeCol(timeRowOrCol.toUpperCase())
|
|
75
75
|
timeRow = dataRow
|
|
76
76
|
dataCol += indexNum
|
|
77
77
|
nextCell = () => {
|
|
@@ -81,7 +81,7 @@ function generateDirectDataLookup(varLhs, getCellValue, timeRowOrCol, startCell,
|
|
|
81
81
|
} else {
|
|
82
82
|
// Time values are in a row.
|
|
83
83
|
timeCol = dataCol
|
|
84
|
-
timeRow =
|
|
84
|
+
timeRow = decodeRow(timeRowOrCol)
|
|
85
85
|
dataRow += indexNum
|
|
86
86
|
nextCell = () => {
|
|
87
87
|
dataCol++
|
|
@@ -481,6 +481,11 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
481
481
|
validateCallArgs(callExpr, 3)
|
|
482
482
|
break
|
|
483
483
|
|
|
484
|
+
case '_ALLOCATE_BY_PRIORITY':
|
|
485
|
+
validateCallDepth(callExpr, context)
|
|
486
|
+
validateCallArgs(callExpr, 5)
|
|
487
|
+
break
|
|
488
|
+
|
|
484
489
|
case '_DELAY1':
|
|
485
490
|
case '_DELAY1I':
|
|
486
491
|
case '_DELAY3':
|
|
@@ -880,6 +885,9 @@ function visitFunctionCall(v, callExpr, context) {
|
|
|
880
885
|
}
|
|
881
886
|
}
|
|
882
887
|
continue
|
|
888
|
+
} else if (callExpr.fnId === '_ALLOCATE_BY_PRIORITY') {
|
|
889
|
+
// TODO: Throw an error if the last dimension of arg0 does not match last dimension of LHS
|
|
890
|
+
// TODO: Throw an error if the last dimension of arg1 does not match last dimension of LHS
|
|
883
891
|
}
|
|
884
892
|
|
|
885
893
|
context.setArgIndex(index, argModes[index])
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import XLSX from 'xlsx'
|
|
2
|
-
|
|
3
1
|
import { readCsv } from '../_shared/helpers.js'
|
|
4
2
|
import { Subscript } from '../_shared/subscript.js'
|
|
3
|
+
import { decodeCell } from '../_shared/xlsx.js'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Read the dimension definitions from the given model.
|
|
@@ -46,7 +45,7 @@ export function readDimensionDefs(parsedModel) {
|
|
|
46
45
|
*/
|
|
47
46
|
export function getDirectSubscripts(fileName, tabOrDelimiter, firstCell, lastCell) {
|
|
48
47
|
// If lastCell is a column letter, scan the column, else scan the row
|
|
49
|
-
const dataAddress =
|
|
48
|
+
const dataAddress = decodeCell(firstCell.toUpperCase())
|
|
50
49
|
let col = dataAddress.c
|
|
51
50
|
let row = dataAddress.r
|
|
52
51
|
if (col < 0 || row < 0) {
|