Implement table command to parse tsv, csv, and JSON
This commit is contained in:
@@ -58,6 +58,7 @@ import {If} from "./if.js";
|
||||
import {Unless} from "./unless.js";
|
||||
import {Convert} from "./convert.js";
|
||||
import {While} from "./while.js";
|
||||
import {Table} from "./table.js";
|
||||
|
||||
export type Commands = Command<CommandData>[]
|
||||
export const commands: Commands = [
|
||||
@@ -108,6 +109,7 @@ export const commands: Commands = [
|
||||
new Sort,
|
||||
new Split,
|
||||
new Suffix,
|
||||
new Table,
|
||||
new Take,
|
||||
new To,
|
||||
new Trim,
|
||||
|
||||
114
src/vm/commands/table.ts
Normal file
114
src/vm/commands/table.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { parseString } from '@fast-csv/parse'
|
||||
import {Command, ParseContext, wrapDestructured, wrapString} from "./command.js";
|
||||
import {LexInput} from "../lexer.js";
|
||||
import {Awaitable, JSONData} from "../../util/types.js";
|
||||
import {ExecutionError, StrVM} from "../vm.js";
|
||||
import {log} from "../../log.js";
|
||||
|
||||
export type TableData = {
|
||||
source: 'json'|'csv'|'tsv',
|
||||
}
|
||||
|
||||
const parseJsonToTable = (json: string): string[][] => {
|
||||
let data: JSONData
|
||||
try {
|
||||
data = JSON.parse(json)
|
||||
} catch (e: unknown) {
|
||||
log.debug('parseJsonToTable', e)
|
||||
throw new ExecutionError('Could not parse: subject is not valid JSON')
|
||||
}
|
||||
|
||||
if ( !Array.isArray(data) ) {
|
||||
throw new ExecutionError('Could not parse: JSON must be an array of records')
|
||||
}
|
||||
|
||||
let tableData: string[][] = []
|
||||
let orderedKeys: string[] = []
|
||||
for ( const row of data ) {
|
||||
if (
|
||||
typeof row !== 'object'
|
||||
|| !row
|
||||
|| Array.isArray(row)
|
||||
) {
|
||||
throw new ExecutionError('Could not parse: JSON must be of the type {[string]: string|number|boolean}[]')
|
||||
}
|
||||
|
||||
// Add any keys in this row that weren't already present to the end of orderedKeys
|
||||
const missingKeys = Object.keys(row)
|
||||
.filter(k => !orderedKeys.includes(k))
|
||||
|
||||
missingKeys.forEach(k => orderedKeys.push(k))
|
||||
|
||||
// Backfill the missing keys on any existing data
|
||||
for ( const tableRow of tableData ) {
|
||||
for ( const key of missingKeys ) {
|
||||
tableRow.push('')
|
||||
}
|
||||
}
|
||||
|
||||
const tableRow: string[] = []
|
||||
for ( const key of orderedKeys ) {
|
||||
const val = row[key]
|
||||
if ( typeof val === 'object' || typeof val === 'undefined' || typeof val === 'function' || Array.isArray(val) ) {
|
||||
throw new ExecutionError('Could not parse: row values must be string|boolean|number')
|
||||
}
|
||||
|
||||
tableRow.push(val.toString())
|
||||
}
|
||||
tableData.push(tableRow)
|
||||
}
|
||||
return tableData
|
||||
}
|
||||
|
||||
const parseCsvToTable = (csvString: string): Promise<string[][]> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const rows: string[][] = [];
|
||||
|
||||
parseString(csvString, { headers: false })
|
||||
.on('error', (error) => reject(error))
|
||||
.on('data', (row: string[]) => rows.push(row))
|
||||
.on('end', () => resolve(rows));
|
||||
})
|
||||
|
||||
const parseTsvToTable = (tsvString: string): Promise<string[][]> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const rows: string[][] = [];
|
||||
|
||||
parseString(tsvString, { headers: false, delimiter: '\t' })
|
||||
.on('error', (error) => reject(error))
|
||||
.on('data', (row: string[]) => rows.push(row))
|
||||
.on('end', () => resolve(rows));
|
||||
})
|
||||
|
||||
export class Table extends Command<TableData> {
|
||||
attemptParse(context: ParseContext): Awaitable<TableData> {
|
||||
return {
|
||||
source: context.popKeywordInSet(['json', 'csv', 'tsv']).value,
|
||||
}
|
||||
}
|
||||
|
||||
getDisplayName(): string {
|
||||
return 'table'
|
||||
}
|
||||
|
||||
isParseCandidate(token: LexInput): boolean {
|
||||
return this.isKeyword(token, 'table')
|
||||
}
|
||||
|
||||
async execute(vm: StrVM, data: TableData): Promise<StrVM> {
|
||||
return vm.replaceContextMatchingTerm({
|
||||
destructure: async val => {
|
||||
const table: string[][] =
|
||||
data.source === 'json' ? parseJsonToTable(val)
|
||||
: data.source === 'csv' ? (await parseCsvToTable(val))
|
||||
: data.source === 'tsv' ? (await parseTsvToTable(val))
|
||||
: (() => { throw new ExecutionError('Unimplemented table source format') })()
|
||||
|
||||
return table.map(row => {
|
||||
const cells = row.map(cellVal => ({ value: wrapString(cellVal) }))
|
||||
return { value: wrapDestructured(cells) }
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user