├── src ├── renderer │ ├── renderer.js │ ├── components │ │ ├── Tabulator.vue │ │ ├── Button.vue │ │ ├── CollapsibleSection.vue │ │ ├── TextBox.vue │ │ ├── ProgressBar.vue │ │ ├── Canvas.vue │ │ ├── ColorPicker.vue │ │ ├── FilePicker.vue │ │ ├── Select.vue │ │ ├── CheckBox.vue │ │ ├── NumericUpDown.vue │ │ ├── DataPoint.vue │ │ ├── Component.vue │ │ └── Spreadsheet.vue │ ├── utils │ │ ├── object.js │ │ └── canvas.js │ └── App.vue ├── main │ ├── ipc │ │ ├── datalog.js │ │ ├── index.js │ │ ├── files.js │ │ └── render.js │ ├── main.js │ └── dl-reader │ │ ├── dl-reader.js │ │ └── dl-reader.test.js ├── preload │ └── preload.js └── shared │ ├── tree.js │ ├── tree.test.js │ ├── units.js │ ├── formula.js │ └── formula.test.js ├── .gitattributes ├── test-data ├── medium-datalog-0.dl └── small-datalog-0.dl ├── vite.config.mjs ├── vite.preload.config.mjs ├── vite.renderer.config.mjs ├── index.html ├── .eslintrc.json ├── README.md ├── package.json ├── .gitignore ├── vite.main.config.mjs ├── forge.config.js └── LICENSE /src/renderer/renderer.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | test-data/medium-datalog-0.dl filter=lfs diff=lfs merge=lfs -text 2 | test-data/small-datalog-0.dl filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /test-data/medium-datalog-0.dl: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a045a4b45fdd5345e3359b4fbfdc04ab03e75eaf063b2aed7c1adeef14f79540 3 | size 9535000 4 | -------------------------------------------------------------------------------- /test-data/small-datalog-0.dl: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4bd3654cf37c9b67f8dc7f58075903c3379521829a8e37bb63b3bdc37645b29d 3 | size 426484 4 | -------------------------------------------------------------------------------- /src/main/ipc/datalog.js: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import readDatalog from '../dl-reader/dl-reader'; 3 | 4 | export const registerDatalogHandlers = () => { 5 | ipcMain.handle('read-datalog', (_event, path) => readDatalog(path)); 6 | }; 7 | -------------------------------------------------------------------------------- /src/main/ipc/index.js: -------------------------------------------------------------------------------- 1 | import { registerFileHandlers } from './files'; 2 | import { registerDatalogHandlers } from './datalog'; 3 | import { registerRenderHandlers } from './render'; 4 | 5 | export const registerHandlers = () => { 6 | registerFileHandlers(); 7 | registerDatalogHandlers(); 8 | registerRenderHandlers(); 9 | }; 10 | -------------------------------------------------------------------------------- /vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import eslint from 'vite-plugin-eslint'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | { // do not fail on serve (i.e. local development) 8 | ...eslint({ 9 | failOnWarning: false, 10 | failOnError: false, 11 | }), 12 | }, 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /vite.preload.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import eslint from 'vite-plugin-eslint'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | { // do not fail on serve (i.e. local development) 8 | ...eslint({ 9 | failOnWarning: false, 10 | failOnError: false, 11 | }), 12 | }, 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /vite.renderer.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import eslint from 'vite-plugin-eslint'; 4 | 5 | // https://vitejs.dev/config 6 | export default defineConfig({ 7 | plugins: [ 8 | vue(), 9 | { // do not fail on serve (i.e. local development) 10 | ...eslint({ 11 | failOnWarning: false, 12 | failOnError: false, 13 | }), 14 | }, 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hudley 6 | 7 | 8 |
9 | 10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "import/no-extraneous-dependencies": [ 5 | "error", 6 | {"devDependencies": true} 7 | ], 8 | "import/prefer-default-export": "off", 9 | "no-loss-of-precision": "off", 10 | "no-mixed-operators": "off", 11 | "no-console": "off" 12 | }, 13 | "globals": { 14 | "MAIN_WINDOW_VITE_DEV_SERVER_URL": true, 15 | "MAIN_WINDOW_VITE_NAME": true, 16 | "window": true, 17 | "ImageData": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hudley 2 | 3 | Visualize your car's data and overlay it on your video 4 | 5 | 6 | ## How to dev 7 | 8 | ``` 9 | npm i 10 | npm start 11 | ``` 12 | 13 | ## How to test 14 | 15 | ``` 16 | npm test 17 | ``` 18 | 19 | ## How to lint 20 | 21 | ``` 22 | npm run lint 23 | ``` 24 | 25 | and to fix all auto-fixable linting issues 26 | 27 | ``` 28 | npm run lint -- --fix 29 | ``` 30 | 31 | 32 | ## Project Structure 33 | 34 | ``` 35 | ``` 36 | - src/ 37 | - main/ node code 38 | - renderer/ vue code 39 | - shared/ code shared between node and vue 40 | - preload/ expose node code to vue code 41 | ``` 42 | ``` 43 | -------------------------------------------------------------------------------- /src/main/ipc/files.js: -------------------------------------------------------------------------------- 1 | import { dialog, ipcMain } from 'electron'; 2 | import { basename } from 'path'; 3 | 4 | const showOpenDialog = (_event, options) => dialog.showOpenDialog(options) 5 | .then((result) => ({ 6 | ...result, 7 | basenames: result.filePaths.map((p) => basename(p)), 8 | })); 9 | 10 | const showSaveDialog = (_event, options) => dialog.showSaveDialog(options) 11 | .then((result) => ({ 12 | ...result, 13 | basename: basename(result.filePath), 14 | })); 15 | 16 | export const registerFileHandlers = () => { 17 | ipcMain.handle('show-open-dialog', showOpenDialog); 18 | ipcMain.handle('show-save-dialog', showSaveDialog); 19 | }; 20 | -------------------------------------------------------------------------------- /src/preload/preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron'); 2 | 3 | contextBridge.exposeInMainWorld('hudley', { 4 | 5 | showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options), 6 | showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options), 7 | 8 | readDatalog: (path) => ipcRenderer.invoke('read-datalog', path) 9 | .then((datalog) => ({ 10 | ...datalog, 11 | units: Object.entries(datalog.units).reduce((result, [name, unit]) => ({ 12 | ...result, 13 | [name]: Symbol.for(unit), 14 | }), {}), 15 | })), 16 | 17 | initRender: (renderPath, options) => ipcRenderer.invoke('init-render', renderPath, options), 18 | addFrame: (frame, alpha) => ipcRenderer.invoke('add-frame', frame, alpha), 19 | completeRender: () => ipcRenderer.invoke('complete-render'), 20 | }); 21 | -------------------------------------------------------------------------------- /src/renderer/components/Tabulator.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | 33 | 38 | -------------------------------------------------------------------------------- /src/renderer/components/Button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 45 | 46 | -------------------------------------------------------------------------------- /src/renderer/utils/object.js: -------------------------------------------------------------------------------- 1 | export const replaceAtKey = (object, key, newValue) => { 2 | const entries = Object.entries(object); 3 | const index = entries.findIndex(([k]) => k === key); 4 | return [ 5 | ...entries.slice(0, index), 6 | [key, newValue], 7 | ...entries.slice(index + 1), 8 | ].reduce((result, [k, v]) => ({ 9 | ...result, 10 | [k]: v, 11 | }), {}); 12 | }; 13 | 14 | export const replaceAtIndex = (object, index, newValue, newKey) => { 15 | const entries = Object.entries(object); 16 | const key = newKey || entries[index][0]; 17 | return [ 18 | ...entries.slice(0, index), 19 | [key, newValue], 20 | ...entries.slice(index + 1), 21 | ].reduce((result, [k, v]) => ({ 22 | ...result, 23 | [k]: v, 24 | }), {}); 25 | }; 26 | 27 | export const deleteAtIndex = (object, index) => { 28 | const entries = Object.entries(object); 29 | return [ 30 | ...entries.slice(0, index), 31 | ...entries.slice(index + 1), 32 | ].reduce((result, [k, v]) => ({ 33 | ...result, 34 | [k]: v, 35 | }), {}); 36 | }; 37 | -------------------------------------------------------------------------------- /src/renderer/components/CollapsibleSection.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 50 | 51 | -------------------------------------------------------------------------------- /src/main/ipc/render.js: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import WebMWriter from 'webm-writer'; 3 | import { open } from 'fs/promises'; 4 | 5 | let writer; 6 | let outputFileHandle; 7 | 8 | const initRender = async (_event, renderPath, options) => { 9 | if (outputFileHandle) await outputFileHandle.close(); 10 | 11 | outputFileHandle = await open(renderPath, 'w+'); 12 | writer = new WebMWriter({ 13 | ...options, 14 | fd: outputFileHandle.fd, 15 | }); 16 | }; 17 | 18 | const addFrame = (_event, frame, alpha) => { 19 | if (!writer) throw new Error('Writer not initialized. Be sure to call initRender before addFrame.'); 20 | 21 | return Promise.resolve(writer.addFrame(frame, alpha)); 22 | }; 23 | 24 | const completeRender = () => { 25 | if (!writer) throw new Error('Writer not initialized. Be sure to call initRender before completeRender.'); 26 | 27 | return writer.complete().then(() => outputFileHandle.close()); 28 | }; 29 | 30 | export const registerRenderHandlers = () => { 31 | ipcMain.handle('init-render', initRender); 32 | ipcMain.handle('add-frame', addFrame); 33 | ipcMain.handle('complete-render', completeRender); 34 | }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hudley", 3 | "productName": "hudley", 4 | "version": "1.0.0", 5 | "description": "My Electron application description", 6 | "main": ".vite/build/main.js", 7 | "scripts": { 8 | "start": "electron-forge start", 9 | "test": "vitest", 10 | "package": "electron-forge package", 11 | "make": "electron-forge make", 12 | "publish": "electron-forge publish", 13 | "lint": "npx eslint ." 14 | }, 15 | "keywords": [], 16 | "author": "Kaelinator et al.", 17 | "license": "GPLv3", 18 | "devDependencies": { 19 | "@electron-forge/cli": "^7.8.0", 20 | "@electron-forge/maker-deb": "^7.8.0", 21 | "@electron-forge/maker-rpm": "^7.8.0", 22 | "@electron-forge/maker-squirrel": "^7.8.0", 23 | "@electron-forge/maker-zip": "^7.8.0", 24 | "@electron-forge/plugin-auto-unpack-natives": "^7.8.0", 25 | "@electron-forge/plugin-fuses": "^7.8.0", 26 | "@electron-forge/plugin-vite": "^7.8.0", 27 | "@electron/fuses": "^1.8.0", 28 | "@vitejs/plugin-vue": "^5.2.3", 29 | "electron": "35.1.5", 30 | "eslint-config-airbnb": "^19.0.4", 31 | "vite": "^5.4.18", 32 | "vite-plugin-eslint": "^1.8.1", 33 | "vitest": "^3.1.1" 34 | }, 35 | "dependencies": { 36 | "electron-squirrel-startup": "^1.0.1", 37 | "uuid": "^11.1.0", 38 | "vite-plugin-string-replace": "^1.1.3", 39 | "vue": "^3.5.13", 40 | "vue-icons-plus": "^0.1.8", 41 | "webm-writer": "^1.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/components/TextBox.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | 36 | 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # Vite 89 | .vite/ 90 | 91 | # Electron-Forge 92 | out/ 93 | -------------------------------------------------------------------------------- /src/renderer/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 69 | -------------------------------------------------------------------------------- /vite.main.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import eslint from 'vite-plugin-eslint'; 3 | import StringReplace from 'vite-plugin-string-replace'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | { // do not fail on serve (i.e. local development) 9 | ...eslint({ 10 | failOnWarning: false, 11 | failOnError: false, 12 | }), 13 | }, 14 | StringReplace([ 15 | { 16 | /* WebMWriter.js doesn't need window cause it's running in node environment */ 17 | fileName: 'WebMWriter.js', 18 | search: 'window\.', 19 | replace: '', 20 | }, 21 | { 22 | /* WebMWriter.js doesn't need HTMLCanvasElement cause it's running in node environment */ 23 | fileName: 'WebMWriter.js', 24 | search: 'HTMLCanvasElement', 25 | replace: 'Object', 26 | }, 27 | { 28 | /* WebMWriter.js doesn't need HTMLCanvasElement cause it's running in node environment */ 29 | fileName: 'WebMWriter.js', 30 | search: 'videoWidth = frame.width', 31 | replace: 'videoWidth = options.width || frame.width', 32 | }, 33 | { 34 | /* WebMWriter.js doesn't need HTMLCanvasElement cause it's running in node environment */ 35 | fileName: 'WebMWriter.js', 36 | search: 'videoHeight = frame.height', 37 | replace: 'videoHeight = options.height || frame.height', 38 | }, 39 | { 40 | /* Stupid bug corrupting all the webms that took me hours to fix */ 41 | fileName: 'BlobBuffer.js', 42 | search: 'dataArray.buffer', 43 | replace: 'dataArray', 44 | }, 45 | ]), 46 | ], 47 | }); 48 | -------------------------------------------------------------------------------- /src/renderer/components/Canvas.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 32 | 33 | 59 | -------------------------------------------------------------------------------- /src/renderer/components/ColorPicker.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | 37 | 83 | -------------------------------------------------------------------------------- /src/main/main.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import path from 'node:path'; 3 | import started from 'electron-squirrel-startup'; 4 | import { registerHandlers } from './ipc'; 5 | 6 | // Handle creating/removing shortcuts on Windows when installing/uninstalling. 7 | if (started) { 8 | app.quit(); 9 | } 10 | 11 | const createWindow = () => { 12 | // Create the browser window. 13 | const mainWindow = new BrowserWindow({ 14 | width: 800, 15 | height: 600, 16 | webPreferences: { 17 | preload: path.join(__dirname, 'preload.js'), 18 | contextIsolation: true, 19 | }, 20 | }); 21 | 22 | mainWindow.setMenuBarVisibility(false); 23 | 24 | // and load the index.html of the app. 25 | if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { 26 | mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); 27 | } else { 28 | mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); 29 | } 30 | 31 | // Open the DevTools. 32 | mainWindow.webContents.openDevTools(); 33 | }; 34 | 35 | // This method will be called when Electron has finished 36 | // initialization and is ready to create browser windows. 37 | // Some APIs can only be used after this event occurs. 38 | app.whenReady().then(() => { 39 | createWindow(); 40 | registerHandlers(); 41 | 42 | // On OS X it's common to re-create a window in the app when the 43 | // dock icon is clicked and there are no other windows open. 44 | app.on('activate', () => { 45 | if (BrowserWindow.getAllWindows().length === 0) { 46 | createWindow(); 47 | } 48 | }); 49 | }); 50 | 51 | // Quit when all windows are closed, except on macOS. There, it's common 52 | // for applications and their menu bar to stay active until the user quits 53 | // explicitly with Cmd + Q. 54 | app.on('window-all-closed', () => { 55 | if (process.platform !== 'darwin') { 56 | app.quit(); 57 | } 58 | }); 59 | 60 | // In this file you can include the rest of your app's specific main process 61 | // code. You can also put them in separate files and import them here. 62 | -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | const { FusesPlugin } = require('@electron-forge/plugin-fuses'); 2 | const { FuseV1Options, FuseVersion } = require('@electron/fuses'); 3 | 4 | module.exports = { 5 | packagerConfig: { 6 | asar: true, 7 | }, 8 | rebuildConfig: {}, 9 | makers: [ 10 | { 11 | name: '@electron-forge/maker-squirrel', 12 | config: {}, 13 | }, 14 | { 15 | name: '@electron-forge/maker-zip', 16 | platforms: ['darwin'], 17 | }, 18 | { 19 | name: '@electron-forge/maker-deb', 20 | config: {}, 21 | }, 22 | { 23 | name: '@electron-forge/maker-rpm', 24 | config: {}, 25 | }, 26 | ], 27 | plugins: [ 28 | { 29 | name: '@electron-forge/plugin-vite', 30 | config: { 31 | // `build` can specify multiple entry builds, 32 | // which can be Main process, Preload scripts, Worker process, etc. 33 | // If you are familiar with Vite configuration, it will look really familiar. 34 | build: [ 35 | { 36 | // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`. 37 | entry: 'src/main/main.js', 38 | config: 'vite.main.config.mjs', 39 | target: 'main', 40 | }, 41 | { 42 | entry: 'src/preload/preload.js', 43 | config: 'vite.preload.config.mjs', 44 | target: 'preload', 45 | }, 46 | ], 47 | renderer: [ 48 | { 49 | name: 'main_window', 50 | config: 'vite.renderer.config.mjs', 51 | }, 52 | ], 53 | }, 54 | }, 55 | // Fuses are used to enable/disable various Electron functionality 56 | // at package time, before code signing the application 57 | new FusesPlugin({ 58 | version: FuseVersion.V1, 59 | [FuseV1Options.RunAsNode]: false, 60 | [FuseV1Options.EnableCookieEncryption]: true, 61 | [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, 62 | [FuseV1Options.EnableNodeCliInspectArguments]: false, 63 | [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, 64 | [FuseV1Options.OnlyLoadAppFromAsar]: true, 65 | }), 66 | ], 67 | }; 68 | -------------------------------------------------------------------------------- /src/renderer/components/FilePicker.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 40 | 41 | 85 | -------------------------------------------------------------------------------- /src/renderer/components/Select.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | 40 | 115 | -------------------------------------------------------------------------------- /src/shared/tree.js: -------------------------------------------------------------------------------- 1 | export const getParentIndex = (index) => Math.ceil(index / 2) - 1; 2 | 3 | /* 4 | * Returns the parent of the node at index or null if this is root 5 | * Assumes binary tree structured in level order 6 | */ 7 | export const getParent = (tree, index) => { 8 | if (index === 0) return null; 9 | 10 | return tree[getParentIndex(index)]; 11 | }; 12 | 13 | export const getLeftChildIndex = (index) => index * 2 + 1; 14 | 15 | export const getLeftChild = (tree, index) => { 16 | const leftChildIndex = getLeftChildIndex(index); 17 | 18 | if (leftChildIndex >= tree.length) { 19 | return null; 20 | } 21 | 22 | return tree[leftChildIndex]; 23 | }; 24 | 25 | export const getRightChildIndex = (index) => index * 2 + 2; 26 | 27 | export const getRightChild = (tree, index) => { 28 | const rightChildIndex = getRightChildIndex(index); 29 | 30 | if (rightChildIndex >= tree.length) { 31 | return null; 32 | } 33 | 34 | return tree[rightChildIndex]; 35 | }; 36 | 37 | export const isLeftChild = (index) => index % 2 === 1; 38 | export const isRightChild = (index) => index !== 0 && index % 2 === 0; 39 | 40 | /** 41 | * Creates array from a to b, inclusive 42 | */ 43 | const range = (a, b) => Array(b - a + 1).fill().map((_, i) => a + i); 44 | 45 | const copyDown = (tree, left, right) => { 46 | const areAllNull = range(left, right) 47 | .every((i) => tree[i] === null || i >= tree.length); 48 | 49 | let newTree = tree.concat(); // make copy 50 | if (!areAllNull) { 51 | newTree = copyDown(tree, getLeftChildIndex(left), getRightChildIndex(right)); 52 | } 53 | 54 | if (right >= newTree.length) { 55 | // resize 56 | newTree = newTree.concat(range(newTree.length, right).fill(null)); 57 | } 58 | 59 | if (left === right) { 60 | newTree[left] = null; 61 | return newTree; 62 | } 63 | 64 | const startParent = getParentIndex(left); 65 | range(0, Math.floor((right - left) / 2)) 66 | .forEach((i) => { 67 | newTree[left + i] = newTree[startParent + i] !== undefined ? newTree[startParent + i] : null; 68 | newTree[startParent + i] = null; 69 | }); 70 | 71 | return newTree; 72 | }; 73 | 74 | /** 75 | * returns modified tree such that a parent is inserted at index 76 | * and node at index becomes the new node's left child and all 77 | * descendants are updated accordingly 78 | */ 79 | export const insertParent = (tree, index) => copyDown(tree, index, index); 80 | 81 | const getLeftSubTreeLength = (length) => { 82 | if (length === 0) return 0; 83 | 84 | const completeLevels = Math.floor(Math.log2(length + 1)); 85 | const rightTreeLength = 2 ** (completeLevels - 1) - 1; 86 | const nodesInLastLevel = length - (2 ** completeLevels - 1); 87 | 88 | return length - rightTreeLength - Math.max(0, nodesInLastLevel - 2 ** (completeLevels - 1)) - 1; 89 | }; 90 | 91 | export const getLeftSubTree = (tree) => new Array(getLeftSubTreeLength(tree.length)) 92 | .fill() 93 | .map((_, i) => (i === 0 94 | ? tree[1] 95 | : tree[i + (1 << Math.floor(Math.log2(i + 1)))])); 96 | 97 | const getRightSubTreeLength = (length) => { 98 | if (length === 0) return 0; 99 | return length - getLeftSubTreeLength(length) - 1; 100 | }; 101 | 102 | export const getRightSubTree = (tree) => new Array(getRightSubTreeLength(tree.length)) 103 | .fill() 104 | .map((_, i) => (i === 0 105 | ? tree[2] 106 | : tree[i + (1 << (Math.floor(Math.log2(i + 1)) + 1))])); 107 | -------------------------------------------------------------------------------- /src/renderer/components/CheckBox.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | 38 | 145 | -------------------------------------------------------------------------------- /src/renderer/components/NumericUpDown.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 61 | 62 | 141 | -------------------------------------------------------------------------------- /src/renderer/utils/canvas.js: -------------------------------------------------------------------------------- 1 | import { abbreviations, conversions } from '../../shared/units'; 2 | 3 | const convertAlphaToGrayscaleImage = (context) => { 4 | const source = context.getImageData(0, 0, context.canvas.width, context.canvas.height).data; 5 | const transparencyMap = source.map((value, i) => { 6 | const rgbaIndex = i % 4; // 0 = r, 1 = g, 2 = b, 3 = a 7 | return rgbaIndex < 3 ? source[i + (3 - rgbaIndex)] : 255; 8 | }); 9 | const buf = new ImageData(transparencyMap, context.canvas.width, context.canvas.height); 10 | context.putImageData(buf, 0, 0); 11 | return context.canvas.toDataURL('image/webp'); 12 | }; 13 | 14 | export const renderFrame = (context, components, dataPointValues, dataPointUnits) => { 15 | const { canvas } = context; 16 | 17 | context.clearRect(0, 0, canvas.width, canvas.height); 18 | 19 | components.forEach((component) => { 20 | context.font = `${component.size}px '${component.font}', sans-serif`; 21 | context.fillStyle = component.fill; 22 | context.textAlign = component.justify; 23 | context.textBaseline = component.align; 24 | context.strokeStyle = component.stroke; 25 | context.lineWidth = component.strokeWeight; 26 | 27 | const fromUnit = dataPointUnits[component.dataPoint]; 28 | const toUnit = component.unitOfMeasure; 29 | 30 | const value = (fromUnit !== toUnit) 31 | ? conversions[fromUnit][toUnit](dataPointValues[component.dataPoint]) 32 | : dataPointValues[component.dataPoint]; 33 | 34 | const text = `${component.label}${value.toFixed(component.decimalPlaces)}${component.showUnitOfMeasure ? abbreviations[component.unitOfMeasure] : ''}`; 35 | 36 | 37 | if (component.strokeWeight > 0) { 38 | context.strokeText(text, component.x, component.y); 39 | } 40 | 41 | context.fillText(text, component.x, component.y); 42 | }); 43 | 44 | const frame = context.canvas.toDataURL('image/webp'); 45 | 46 | return { 47 | frame, 48 | alpha: convertAlphaToGrayscaleImage(context), 49 | }; 50 | }; 51 | 52 | export const render = (context, components, datalog, { 53 | startPoint, endPoint, framerate, renderPath, width, height 54 | }) => { 55 | let cancelled = false; 56 | let progressHandler; 57 | let doneHandler; 58 | 59 | const api = {}; 60 | 61 | api.onProgress = (f) => { 62 | progressHandler = f; 63 | return api; 64 | }; 65 | 66 | api.onDone = (f) => { 67 | doneHandler = f; 68 | return api; 69 | }; 70 | 71 | api.cancel = () => { 72 | cancelled = true; 73 | }; 74 | 75 | window.hudley.initRender(renderPath, { 76 | quality: 0.99999, 77 | frameRate: framerate, 78 | transparent: true, 79 | width, 80 | height, 81 | }).then(async () => { 82 | const startTime = Date.now(); 83 | let currentRtc = datalog.points[0].rtc; 84 | const stepDuration = 1000 / framerate; // ms between frames 85 | 86 | /* eslint-disable no-await-in-loop */ 87 | for (let i = startPoint; i < endPoint; i += 1) { 88 | const { frame, alpha } = renderFrame(context, components, datalog.points[i], datalog.units); 89 | await window.hudley.addFrame(frame, alpha); 90 | currentRtc += stepDuration; 91 | while (i !== endPoint - 1 && currentRtc < datalog.points[i + 1].rtc) { 92 | await window.hudley.addFrame(frame, alpha); 93 | currentRtc += stepDuration; 94 | } 95 | if (i !== startPoint) { 96 | progressHandler({ 97 | frame: i - startPoint, 98 | totalFrames: endPoint - startPoint, 99 | progress: (i - startPoint) / (endPoint - startPoint), 100 | elapsedTime: Date.now() - startTime, 101 | estimatedRunTime: (endPoint - startPoint) / (i - startPoint) * (Date.now() - startTime), 102 | }); 103 | } 104 | if (cancelled) { 105 | break; 106 | } 107 | } 108 | /* eslint-enable no-await-in-loop */ 109 | 110 | await window.hudley.completeRender(); 111 | doneHandler(); 112 | console.log('done'); 113 | }); 114 | 115 | return api; 116 | }; 117 | -------------------------------------------------------------------------------- /src/renderer/components/DataPoint.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 114 | 115 | 175 | -------------------------------------------------------------------------------- /src/shared/tree.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe } from 'vitest'; 2 | import { 3 | getParentIndex, getLeftChildIndex, getRightChildIndex, insertParent, getLeftSubTree, getRightSubTree, 4 | } from './tree'; 5 | 6 | describe('getParentIndex', () => { 7 | test('returns negative if root', () => { 8 | expect(getParentIndex(0)).toBeLessThan(0); 9 | }); 10 | 11 | test('returns index of parent', () => { 12 | expect(getParentIndex(1)).toBe(0); 13 | expect(getParentIndex(2)).toBe(0); 14 | 15 | expect(getParentIndex(3)).toBe(1); 16 | expect(getParentIndex(4)).toBe(1); 17 | expect(getParentIndex(5)).toBe(2); 18 | expect(getParentIndex(6)).toBe(2); 19 | 20 | expect(getParentIndex(7)).toBe(3); 21 | expect(getParentIndex(8)).toBe(3); 22 | expect(getParentIndex(9)).toBe(4); 23 | expect(getParentIndex(10)).toBe(4); 24 | expect(getParentIndex(11)).toBe(5); 25 | expect(getParentIndex(12)).toBe(5); 26 | expect(getParentIndex(13)).toBe(6); 27 | expect(getParentIndex(14)).toBe(6); 28 | }); 29 | }); 30 | 31 | describe('getLeftChildIndex', () => { 32 | test('returns index of left child', () => { 33 | expect(getLeftChildIndex(0)).toBe(1); 34 | 35 | expect(getLeftChildIndex(1)).toBe(3); 36 | expect(getLeftChildIndex(2)).toBe(5); 37 | 38 | expect(getLeftChildIndex(3)).toBe(7); 39 | expect(getLeftChildIndex(4)).toBe(9); 40 | expect(getLeftChildIndex(5)).toBe(11); 41 | expect(getLeftChildIndex(6)).toBe(13); 42 | }); 43 | }); 44 | 45 | describe('getRightChildIndex', () => { 46 | test('returns index of right child', () => { 47 | expect(getRightChildIndex(0)).toBe(2); 48 | 49 | expect(getRightChildIndex(1)).toBe(4); 50 | expect(getRightChildIndex(2)).toBe(6); 51 | 52 | expect(getRightChildIndex(3)).toBe(8); 53 | expect(getRightChildIndex(4)).toBe(10); 54 | expect(getRightChildIndex(5)).toBe(12); 55 | expect(getRightChildIndex(6)).toBe(14); 56 | }); 57 | }); 58 | 59 | describe('insertParent', () => { 60 | test('inserts at empty tree', () => { 61 | expect(insertParent([], 0)).toEqual([null]); 62 | }); 63 | 64 | test('inserts at root node', () => { 65 | expect(insertParent([0], 0)).toEqual([null, 0, null]); 66 | expect(insertParent([0, 1], 0)).toEqual([null, 0, null, 1, null, null, null]); 67 | expect(insertParent([0, null, 1], 0)).toEqual([null, 0, null, null, 1, null, null]); 68 | expect(insertParent([0, 1, 2], 0)).toEqual([null, 0, null, 1, 2, null, null]); 69 | }); 70 | 71 | test('inserts at left node', () => { 72 | expect(insertParent([0, 1], 1)).toEqual([0, null, null, 1, null]); 73 | expect(insertParent([0, 1, 2, 3], 3)).toEqual([0, 1, 2, null, null, null, null, 3, null]); 74 | }); 75 | 76 | test('inserts at right node', () => { 77 | expect(insertParent([0, 1, 2], 2)).toEqual([0, 1, null, null, null, 2, null]); 78 | expect(insertParent([0, 1, 2, 3, 4, 5, 6], 6)).toEqual([0, 1, 2, 3, 4, 5, null, null, null, null, null, null, null, 6, null]); 79 | }); 80 | 81 | test('inserts at nested node', () => { 82 | expect(insertParent([0, 1, 2, 3, 4, 5, 6], 1)).toEqual([0, null, 2, 1, null, 5, 6, 3, 4, null, null]); 83 | expect(insertParent([0, 1, 2, 3, 4, 5, 6], 2)).toEqual([0, 1, null, 3, 4, 2, null, null, null, null, null, 5, 6, null, null]); 84 | }); 85 | 86 | test('inserts and moves descendants', () => { 87 | expect(insertParent([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], 2)) 88 | .toEqual([0, 1, null, 3, 4, 2, null, 7, 8, 9, 10, 5, 6, null, null, null, null, null, null, null, null, null, null, 11, 12, 13, 14, null, null, null, null]); 89 | }); 90 | 91 | test('inserts at nonexistent node', () => { 92 | expect(insertParent([0, 1], 2)).toEqual([0, 1, null]); 93 | expect(insertParent([0, 1, null], 2)).toEqual([0, 1, null]); 94 | }); 95 | }); 96 | 97 | describe('getLeftSubTree', () => { 98 | test('works for simple tree', () => { 99 | expect(getLeftSubTree([])).toEqual([]); 100 | expect(getLeftSubTree([0])).toEqual([]); 101 | expect(getLeftSubTree([0, 1])).toEqual([1]); 102 | expect(getLeftSubTree([0, 1, 2])).toEqual([1]); 103 | }); 104 | test('works for many levels', () => { 105 | expect(getLeftSubTree([0, 1, 2, 3, 4, 5, 6])).toEqual([1, 3, 4]); 106 | expect(getLeftSubTree([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])).toEqual([1, 3, 4, 7, 8, 9, 10]); 107 | expect(getLeftSubTree([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])).toEqual([1, 3, 4, 7, 8, 9, 10]); 108 | }); 109 | }); 110 | 111 | describe('getRightSubTree', () => { 112 | test('works for simple tree', () => { 113 | expect(getRightSubTree([])).toEqual([]); 114 | expect(getRightSubTree([0])).toEqual([]); 115 | expect(getRightSubTree([0, 1])).toEqual([]); 116 | expect(getRightSubTree([0, 1, 2])).toEqual([2]); 117 | }); 118 | test('works for many levels', () => { 119 | expect(getRightSubTree([0, 1, 2, 3, 4, 5, 6])).toEqual([2, 5, 6]); 120 | expect(getRightSubTree([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])).toEqual([2, 5, 6, 11]); 121 | expect(getRightSubTree([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])).toEqual([2, 5, 6, 11, 12, 13, 14]); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /src/shared/units.js: -------------------------------------------------------------------------------- 1 | export const units = { 2 | /* temperature */ 3 | F: Symbol.for('fahrenheit'), 4 | C: Symbol.for('celcius'), 5 | 6 | /* pressure */ 7 | PSIA: Symbol.for('pounds per square inch absolute '), 8 | PSIG: Symbol.for('pounds per square inch guage '), 9 | KPA: Symbol.for('kilopascals absolute'), 10 | BAR: Symbol.for('bar absolute'), 11 | 12 | /* flow */ 13 | GPH: Symbol.for('gallons per hour'), 14 | LPH: Symbol.for('liters per hour'), 15 | GLBPH: Symbol.for('pounds of gasoline per hour'), 16 | 17 | /* time */ 18 | MS: Symbol.for('milliseconds'), 19 | S: Symbol.for('seconds'), 20 | H: Symbol.for('hour'), 21 | 22 | /* distance */ 23 | FT: Symbol.for('feet'), 24 | IN: Symbol.for('inch'), 25 | M: Symbol.for('meter'), 26 | MM: Symbol.for('millimeter'), 27 | 28 | /* speed */ 29 | MPH: Symbol.for('miles per hour'), 30 | KPH: Symbol.for('kilometers per hour'), 31 | 32 | /* part */ 33 | TO1: Symbol.for('ratio to 1'), 34 | PERCENT: Symbol.for('percent'), 35 | 36 | /* frequency */ 37 | RPM: Symbol.for('rotations per minute'), 38 | HZ: Symbol.for('hertz'), 39 | 40 | /* angle */ 41 | DEG: Symbol.for('degrees'), 42 | RAD: Symbol.for('radians'), 43 | 44 | /* charge */ 45 | V: Symbol.for('volts'), 46 | 47 | /* current */ 48 | A: Symbol.for('amperes'), 49 | 50 | /* resistance */ 51 | OHM: Symbol.for('ohms'), 52 | 53 | /* None */ 54 | DIMENSIONLESS: Symbol.for('dimensionless'), 55 | }; 56 | 57 | const { 58 | F, 59 | C, 60 | PSIA, 61 | PSIG, 62 | KPA, 63 | BAR, 64 | GPH, 65 | LPH, 66 | GLBPH, 67 | MS, 68 | S, 69 | H, 70 | FT, 71 | IN, 72 | M, 73 | MM, 74 | MPH, 75 | KPH, 76 | TO1, 77 | PERCENT, 78 | RPM, 79 | HZ, 80 | DEG, 81 | RAD, 82 | V, 83 | A, 84 | OHM, 85 | DIMENSIONLESS, 86 | } = units; 87 | 88 | export const abbreviations = { 89 | [F]: '\u00B0F', 90 | [C]: '\u00B0C', 91 | [PSIA]: 'psia', 92 | [PSIG]: 'psig', 93 | [KPA]: 'kPa', 94 | [BAR]: 'bar', 95 | [GPH]: 'gph', 96 | [LPH]: 'Lph', 97 | [GLBPH]: 'lb/hr', 98 | [MS]: 'ms', 99 | [S]: 's', 100 | [H]: 'hr', 101 | [FT]: '\'', 102 | [IN]: '"', 103 | [M]: 'm', 104 | [MM]: 'mm', 105 | [MPH]: 'mph', 106 | [KPH]: 'kph', 107 | [TO1]: ':1', 108 | [PERCENT]: '%', 109 | [RPM]: 'rpm', 110 | [HZ]: 'Hz', 111 | [DEG]: '\u00B0', 112 | [RAD]: 'rad', 113 | [V]: 'V', 114 | [A]: 'A', 115 | [OHM]: '\u03A9', 116 | [DIMENSIONLESS]: '', 117 | }; 118 | 119 | /** 120 | * list of available unit conversions 121 | * useage: conversions[from][to](value) 122 | */ 123 | export const conversions = { 124 | [F]: { 125 | [C]: t => 0.55556 * (t - 32), 126 | }, 127 | 128 | [C]: { 129 | [F]: t => 1.8 * t + 32, 130 | }, 131 | 132 | [PSIA]: { 133 | [PSIG]: p => p - 14.696, 134 | [KPA]: p => p * 6.894757, 135 | [BAR]: p => p * 0.06894757, 136 | }, 137 | 138 | [PSIG]: { 139 | [PSIA]: p => p + 14.696, 140 | [KPA]: p => p * 6.894757 + 101.325, 141 | [BAR]: p => p * 0.06894757 + 1.01325, 142 | }, 143 | 144 | [KPA]: { 145 | [PSIA]: p => p * 0.1450377, 146 | [PSIG]: p => p * 0.1450377 - 14.696, 147 | [BAR]: p => p * 0.01, 148 | }, 149 | 150 | [BAR]: { 151 | [PSIA]: p => p * 14.50377, 152 | [PSIG]: p => p * 14.50377 - 14.696, 153 | [KPA]: p => p * 100, 154 | }, 155 | 156 | [GPH]: { 157 | [LPH]: q => q * 3.785412, 158 | [GLBPH]: q => q * 6.073, 159 | }, 160 | 161 | [LPH]: { 162 | [GPH]: q => q * 0.264172, 163 | [GLBPH]: q => q * 1.60432, 164 | }, 165 | 166 | [GLBPH]: { 167 | [GPH]: q => q * 0.164663, 168 | [LPH]: q => q * 0.623318, 169 | }, 170 | 171 | [MS]: { 172 | [S]: t => t * 0.001, 173 | [H]: t => t * 2.777778E-7, 174 | }, 175 | 176 | [S]: { 177 | [MS]: t => t * 1000, 178 | [H]: t => t * 2.777778E-4, 179 | }, 180 | 181 | [H]: { 182 | [MS]: t => t * 3.6E6, 183 | [S]: t => t * 3600, 184 | }, 185 | 186 | [FT]: { 187 | [IN]: l => l * 12, 188 | [M]: l => 0.3048, 189 | [MM]: l => 304.8, 190 | }, 191 | 192 | [IN]: { 193 | [FT]: l => l * 0.083333, 194 | [M]: l => l * 0.0254, 195 | [MM]: l => l * 25.4, 196 | }, 197 | 198 | [M]: { 199 | [FT]: l => l * 3.28084, 200 | [IN]: l => l * 39.37008, 201 | [MM]: l => l * 1000, 202 | }, 203 | 204 | [MM]: { 205 | [FT]: l => l * 0.00328084, 206 | [IN]: l => l * 0.03937008, 207 | [M]: l => l * 0.001, 208 | }, 209 | 210 | [MPH]: { 211 | [KPH]: v => v * 1.609344, 212 | }, 213 | 214 | [KPH]: { 215 | [MPH]: v => v * 0.6213712, 216 | }, 217 | 218 | [TO1]: { 219 | [PERCENT]: p => p * 100, 220 | }, 221 | 222 | [PERCENT]: { 223 | [TO1]: p => p * 0.01, 224 | }, 225 | 226 | [RPM]: { 227 | [HZ]: r => r * 0.0166667, 228 | }, 229 | 230 | [HZ]: { 231 | [RPM]: r => r * 60, 232 | }, 233 | 234 | [DEG]: { 235 | [RAD]: a => a * 0.01745329, 236 | }, 237 | 238 | [RAD]: { 239 | [DEG]: a => a * 57.29578, 240 | }, 241 | 242 | [V]: {}, 243 | [A]: {}, 244 | [OHM]: {}, 245 | 246 | /* passthrough */ 247 | [DIMENSIONLESS]: Object.values(units).reduce((o, u) => ({ ...o, [u]: v => v }), {}) 248 | }; 249 | -------------------------------------------------------------------------------- /src/main/dl-reader/dl-reader.js: -------------------------------------------------------------------------------- 1 | import { open } from 'node:fs/promises'; 2 | import { units } from '../../shared/units'; 3 | 4 | const DATALOG_SIGNATURE = 0x95365F; 5 | const DATAPOINT_LENGTH = 0x9D0; 6 | const DATAPOINTS_START_OFFSET = 0x2EE4; 7 | 8 | const verifySignature = ({ bytesRead, buffer }) => { 9 | if (bytesRead < 0x8) { 10 | throw new Error('File ended abruptly'); 11 | } 12 | 13 | const signature = Number(buffer.readBigInt64LE(0)); 14 | if (signature !== DATALOG_SIGNATURE) { 15 | throw new Error(`File does not match typical datalog signature. ${signature} != ${DATALOG_SIGNATURE}`); 16 | } 17 | }; 18 | 19 | const getUntilNullTerminatedString = ({ bytesRead, buffer }) => { 20 | if (bytesRead <= 0) { 21 | throw new Error('File ended abruptly'); 22 | } 23 | 24 | const nullTerminatorPosition = buffer.indexOf('\0'); 25 | 26 | return buffer.toString('utf8', 0, nullTerminatorPosition); 27 | }; 28 | 29 | const getDataPoint = ({ bytesRead, buffer }) => { 30 | if (bytesRead < DATAPOINT_LENGTH) { 31 | throw new Error('File ended abruptly'); 32 | } 33 | 34 | return { 35 | rtc: buffer.readInt32LE(0x8), 36 | rpm: buffer.readFloatLE(0x10), 37 | injPW: buffer.readFloatLE(0x18), 38 | dutyCycle: buffer.readFloatLE(0x20), 39 | cLComp: buffer.readFloatLE(0x28), 40 | targetAFR: buffer.readFloatLE(0x30), 41 | targetLambda: buffer.readFloatLE(0x38), 42 | afr: buffer.readFloatLE(0x40), 43 | lambda: buffer.readFloatLE(0x48), 44 | airTempEnr: buffer.readFloatLE(0x50), 45 | coolantEnr: buffer.readFloatLE(0x58), 46 | coolantAFROffset: buffer.readFloatLE(0x60), 47 | afterstartEnr: buffer.readFloatLE(0x68), 48 | currentLearn: buffer.readFloatLE(0x70), 49 | cLStatus: buffer.readFloatLE(0x78), 50 | learnStatus: buffer.readFloatLE(0x80), 51 | fuelEconomy: buffer.readFloatLE(0x88), 52 | fuelFlow: buffer.readFloatLE(0x90), 53 | mapROC: buffer.readFloatLE(0x98), 54 | tpsROC: buffer.readFloatLE(0xA0), 55 | tuningChange: buffer.readFloatLE(0xA8), 56 | estimatedVE: buffer.readFloatLE(0xB0), 57 | mainRevLimit: buffer.readFloatLE(0xB8), 58 | revLimit1: buffer.readFloatLE(0xC0), 59 | launchRetard: buffer.readFloatLE(0xC8), 60 | ignitionTiming: buffer.readFloatLE(0xD0), 61 | knockRetard: buffer.readFloatLE(0xD8), 62 | knockLevel: buffer.readFloatLE(0xE0), 63 | sparkPad2: buffer.readFloatLE(0xE8), 64 | iacPosition: buffer.readFloatLE(0xF0), 65 | targetIdleSpeed: buffer.readFloatLE(0xF8), 66 | map: buffer.readFloatLE(0x100), 67 | tps: buffer.readFloatLE(0x108), 68 | mat: buffer.readFloatLE(0x110), 69 | cts: buffer.readFloatLE(0x118), 70 | baro: buffer.readFloatLE(0x120), 71 | battery: buffer.readFloatLE(0x128), 72 | oilPressure: buffer.readFloatLE(0x130), 73 | fuelPressure: buffer.readFloatLE(0x138), 74 | pedalPosition: buffer.readFloatLE(0x140), 75 | }; 76 | }; 77 | 78 | export default async (datalogPath) => { 79 | const file = await open(datalogPath, 'r'); 80 | let data; 81 | 82 | try { 83 | await file 84 | .read(Buffer.alloc(0x8), { position: 0 }) 85 | .then(verifySignature); 86 | 87 | const { size } = await file.stat(); 88 | const pointCount = Math.floor((size - DATAPOINTS_START_OFFSET) / DATAPOINT_LENGTH); 89 | 90 | data = { 91 | tuneFileName: await file 92 | .read(Buffer.alloc(0x80), { position: 0x324 }) 93 | .then(getUntilNullTerminatedString), 94 | 95 | units: { 96 | rtc: Symbol.keyFor(units.MS), 97 | rpm: Symbol.keyFor(units.RPM), 98 | injPW: Symbol.keyFor(units.MS), 99 | dutyCycle: Symbol.keyFor(units.PERCENT), 100 | cLComp: Symbol.keyFor(units.PERCENT), 101 | targetAFR: Symbol.keyFor(units.TO1), 102 | targetLambda: Symbol.keyFor(units.TO1), 103 | afr: Symbol.keyFor(units.TO1), 104 | lambda: Symbol.keyFor(units.TO1), 105 | airTempEnr: Symbol.keyFor(units.PERCENT), 106 | coolantEnr: Symbol.keyFor(units.PERCENT), 107 | coolantAFROffset: Symbol.keyFor(units.TO1), 108 | afterstartEnr: Symbol.keyFor(units.PERCENT), 109 | currentLearn: Symbol.keyFor(units.PERCENT), 110 | cLStatus: Symbol.keyFor(units.DIMENSIONLESS), 111 | learnStatus: Symbol.keyFor(units.DIMENSIONLESS), 112 | fuelEconomy: Symbol.keyFor(units.DIMENSIONLESS), // no idea 113 | fuelFlow: Symbol.keyFor(units.GLBPH), 114 | mapROC: Symbol.keyFor(units.KPA), // per second or per 10 seconds 115 | tpsROC: Symbol.keyFor(units.PERCENT), // per second 116 | tuningChange: Symbol.keyFor(units.DIMENSIONLESS), // no idea 117 | estimatedVE: Symbol.keyFor(units.PERCENT), 118 | mainRevLimit: Symbol.keyFor(units.DIMENSIONLESS), 119 | revLimit1: Symbol.keyFor(units.DIMENSIONLESS), // no idea 120 | launchRetard: Symbol.keyFor(units.DIMENSIONLESS), // no idea, 121 | ignitionTiming: Symbol.keyFor(units.DEG), 122 | knockRetard: Symbol.keyFor(units.DEG), 123 | knockLevel: Symbol.keyFor(units.PERCENT), 124 | sparkPad2: Symbol.keyFor(units.DIMENSIONLESS), // no idea 125 | iacPosition: Symbol.keyFor(units.PERCENT), 126 | targetIdleSpeed: Symbol.keyFor(units.RPM), 127 | map: Symbol.keyFor(units.KPA), 128 | tps: Symbol.keyFor(units.PERCENT), 129 | mat: Symbol.keyFor(units.F), 130 | cts: Symbol.keyFor(units.F), 131 | baro: Symbol.keyFor(units.KPA), 132 | battery: Symbol.keyFor(units.V), 133 | oilPressure: Symbol.keyFor(units.PSIG), 134 | fuelPressure: Symbol.keyFor(units.PSIG), 135 | pedalPosition: Symbol.keyFor(units.PERCENT), 136 | }, 137 | 138 | points: await Promise.all( 139 | new Array(pointCount) 140 | .fill(0) 141 | .map((_, i) => DATAPOINTS_START_OFFSET + i * DATAPOINT_LENGTH) 142 | .map((position) => file 143 | .read(Buffer.alloc(DATAPOINT_LENGTH), { position }) 144 | .then(getDataPoint)), 145 | ) 146 | , 147 | }; 148 | } finally { 149 | await file.close(); 150 | } 151 | 152 | return data; 153 | }; 154 | -------------------------------------------------------------------------------- /src/shared/formula.js: -------------------------------------------------------------------------------- 1 | import { 2 | getParent, getParentIndex, insertParent, getRightChildIndex, getLeftSubTree, getRightSubTree, 3 | } from './tree'; 4 | 5 | /* 6 | * Allowed characters: a-z, A-Z, 0-9, +, -, /, *, ^, (, ) 7 | */ 8 | export const types = { 9 | IDENTIFIER: Symbol.for('identifier'), 10 | NUMBER: Symbol.for('number'), 11 | OPERATOR: Symbol.for('operator'), 12 | PARENTHESIS: Symbol.for('parenthesis'), 13 | NOOP: Symbol.for('noop'), 14 | }; 15 | 16 | const { 17 | IDENTIFIER, NUMBER, OPERATOR, PARENTHESIS, NOOP, 18 | } = types; 19 | 20 | const isAlpha = (charCode) => (charCode >= 65 && charCode <= 90) || (charCode >= 97 && charCode <= 122); 21 | const isDigit = (charCode) => charCode >= 48 && charCode <= 57; 22 | const isDecimal = (charCode) => charCode === 46; 23 | const isWhitespace = (charCode) => charCode === 9 || charCode === 32; 24 | const isOperator = (charCode) => charCode === 42 || charCode === 43 || charCode === 45 || charCode === 47 || charCode === 94; 25 | const isParenthesis = (charCode) => charCode === 40 || charCode === 41; 26 | export const parse = (formula) => { 27 | if (formula.length === 0) { 28 | return []; 29 | } 30 | 31 | let i = 0; 32 | let token = ''; 33 | let type; 34 | for (i; i < formula.length; i += 1) { 35 | const char = formula[i]; 36 | const charCode = formula.charCodeAt(i); 37 | if (!type && (isDigit(charCode) || isDecimal(charCode))) { 38 | type = NUMBER; 39 | token += char; 40 | continue; 41 | } 42 | 43 | if (type === NUMBER && (isDigit(charCode) || isDecimal(charCode))) { 44 | token += char; 45 | continue; 46 | } 47 | 48 | if (!type && isAlpha(charCode)) { 49 | type = IDENTIFIER; 50 | token += char; 51 | continue; 52 | } 53 | 54 | if (type === IDENTIFIER && (isAlpha(charCode) || isDigit(charCode))) { 55 | token += char; 56 | continue; 57 | } 58 | 59 | if (!type && isOperator(charCode)) { 60 | type = OPERATOR; 61 | token += char; 62 | continue; 63 | } 64 | 65 | if (!type && isParenthesis(charCode)) { 66 | type = PARENTHESIS; 67 | token += char; 68 | continue; 69 | } 70 | 71 | if (isWhitespace(charCode) && type) { 72 | break; 73 | } 74 | 75 | if (isWhitespace(charCode)) { 76 | continue; 77 | } 78 | 79 | break; 80 | } 81 | 82 | if (!token.length) { 83 | return []; 84 | } 85 | 86 | const value = type === NUMBER 87 | ? +token 88 | : token; 89 | 90 | if (type === NUMBER && Number.isNaN(value)) { 91 | throw Error(`Error parsing token: '${token}' is not a number`); 92 | } 93 | 94 | return [ 95 | { type, value }, 96 | ...parse(formula.slice(i)), 97 | ]; 98 | }; 99 | 100 | const PRIORITY = { 101 | '(': 0, 102 | ')': 0, 103 | '^': 1, 104 | '*': 2, 105 | '/': 2, 106 | '+': 3, 107 | '-': 3, 108 | }; 109 | 110 | /* 111 | * If number: 112 | * - fill current node 113 | * If operator: 114 | * - If operator has lower priority than or equal priority to current node's parent, then move to parent 115 | * - make current node child of new operator parent, then move to right child 116 | * 117 | * or something like that 118 | * 119 | * Returns binary tree structured in level order 120 | * e.g. 121 | * a 122 | * / \ 123 | * / \ 124 | * b c 125 | * / / \ 126 | * d f g 127 | * = [a, b, c, d, null, f, g] 128 | */ 129 | export const infixToTree = (tokens, tree = [], index = 0) => { 130 | if (tokens.length <= 0) { 131 | return tree; 132 | } 133 | const token = tokens[0]; 134 | 135 | if (token.type === NUMBER || token.type === IDENTIFIER) { 136 | tree[index] = token; 137 | return infixToTree(tokens.slice(1), tree, index); 138 | } 139 | 140 | const parent = getParent(tree, index); 141 | 142 | if (token.type === PARENTHESIS && token.value === ')') { 143 | if (parent && parent.type !== PARENTHESIS) { 144 | return infixToTree(tokens, tree, getParentIndex(index)); 145 | } 146 | return infixToTree(tokens.slice(1), tree, getParentIndex(index)); 147 | } 148 | 149 | if (parent && PRIORITY[token.value] >= PRIORITY[parent.value] && parent.type !== PARENTHESIS) { 150 | index = getParentIndex(index); 151 | } 152 | 153 | // edge case for open parenthesis 154 | if (tree[index] === null || index >= tree.length) { 155 | tree[index] = { type: NOOP }; 156 | } 157 | 158 | const newTree = insertParent(tree, index); 159 | newTree[index] = token; 160 | 161 | return infixToTree(tokens.slice(1), newTree, getRightChildIndex(index)); 162 | }; 163 | 164 | export const evaluate = (tree, values) => { 165 | if (tree.length <= 0) return 0; // not an exit condition 166 | 167 | const node = tree[0]; 168 | if (node === null) { 169 | return null; 170 | } 171 | 172 | if (node.type === NUMBER) { 173 | return node.value; 174 | } 175 | 176 | if (node.type === IDENTIFIER) { 177 | return values[node.value]; 178 | } 179 | 180 | if (node.type === OPERATOR) { 181 | const left = evaluate(getLeftSubTree(tree), values); 182 | const right = evaluate(getRightSubTree(tree), values); 183 | switch (node.value) { 184 | case '+': 185 | return left + right; 186 | 187 | case '-': 188 | if (right === null) return -left; 189 | return left - right; 190 | 191 | case '*': 192 | return left * right; 193 | 194 | case '/': 195 | return left / right; 196 | 197 | case '^': 198 | return left ** right; 199 | 200 | default: throw new Error(`Invalid operator: ${node.value}`); 201 | } 202 | } 203 | 204 | if (node.type === PARENTHESIS) { 205 | const left = evaluate(getLeftSubTree(tree), values); 206 | const right = evaluate(getRightSubTree(tree), values); 207 | if (left === null) return right; 208 | return left * right; 209 | } 210 | 211 | if (node.type === NOOP) { 212 | return null; 213 | } 214 | 215 | throw new Error(`Invalid node type: ${node.type}`); 216 | }; 217 | 218 | export const calculate = (expression, values) => evaluate(infixToTree(parse(expression)), values); 219 | -------------------------------------------------------------------------------- /src/renderer/components/Component.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 133 | 134 | 194 | -------------------------------------------------------------------------------- /src/renderer/components/Spreadsheet.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 180 | 181 | 244 | -------------------------------------------------------------------------------- /src/main/dl-reader/dl-reader.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import readDatalog from './dl-reader'; 3 | 4 | test('reads small datalog', async () => { 5 | // yes I know this is more of an integration test blah blah sue me 6 | const data = await readDatalog('test-data/small-datalog-0.dl'); 7 | expect(data.tuneFileName).toBe('HELLCAT.terx'); 8 | expect(data.points[0]).toMatchObject({ 9 | afr: 14.845837593078613, 10 | afterstartEnr: 125.87004089355469, 11 | airTempEnr: 102.29647827148438, 12 | baro: 102.92281341552734, 13 | battery: 13.926725387573242, 14 | cLComp: 1.549361228942871, 15 | cLStatus: 1, 16 | coolantEnr: 108.43183898925781, 17 | cts: 126.27592468261719, 18 | currentLearn: -28.776464462280273, 19 | dutyCycle: 1.710303783416748, 20 | estimatedVE: 76.9671859741211, 21 | fuelFlow: 8.122174263000488, 22 | fuelPressure: 102.6419677734375, 23 | iacPosition: 0.5326728820800781, 24 | ignitionTiming: 10.076240539550781, 25 | injPW: 2.2092931270599365, 26 | lambda: 1.0099209547042847, 27 | learnStatus: 0, 28 | map: 24.045881271362305, 29 | mapROC: 0, 30 | mat: 94.07595825195312, 31 | oilPressure: 74.45286560058594, 32 | pedalPosition: 0.11093468219041824, 33 | rpm: 1388.312744140625, 34 | rtc: 219295876, 35 | targetAFR: 13.852396011352539, 36 | targetIdleSpeed: 984.3101806640625, 37 | targetLambda: 0.9423398971557617, 38 | tps: 0.18319116532802582, 39 | tpsROC: 0, 40 | }); 41 | expect(data.points.length).toBe(165); 42 | expect(data.points[10]).toMatchObject({ 43 | afr: 15.417254447937012, 44 | afterstartEnr: 125.19358825683594, 45 | airTempEnr: 102.30384063720703, 46 | baro: 102.91476440429688, 47 | battery: 14.336996078491211, 48 | cLComp: 3.582005500793457, 49 | cLStatus: 1, 50 | coolantEnr: 108.434814453125, 51 | cts: 126.26073455810547, 52 | currentLearn: -28.154457092285156, 53 | dutyCycle: 1.5084517002105713, 54 | estimatedVE: 78.41179656982422, 55 | fuelFlow: 7.230020523071289, 56 | fuelPressure: 102.71699523925781, 57 | iacPosition: 2.272350311279297, 58 | ignitionTiming: 4.827414512634277, 59 | injPW: 2.184603452682495, 60 | lambda: 1.048792839050293, 61 | learnStatus: 0, 62 | map: 24.76360321044922, 63 | mapROC: 0, 64 | mat: 93.92056274414062, 65 | oilPressure: 72.69757843017578, 66 | pedalPosition: 0.09400820732116699, 67 | rpm: 1216.692626953125, 68 | rtc: 219296076, 69 | targetAFR: 13.807195663452148, 70 | targetIdleSpeed: 984.34814453125, 71 | targetLambda: 0.9392650127410889, 72 | tps: 0.15979331731796265, 73 | tpsROC: 0, 74 | }); 75 | expect(data.points[100]).toMatchObject({ 76 | afr: 11.580077171325684, 77 | afterstartEnr: 119.12324523925781, 78 | airTempEnr: 102.3572006225586, 79 | baro: 102.85631561279297, 80 | battery: 14.030661582946777, 81 | cLComp: -0.2314765453338623, 82 | cLStatus: 1, 83 | coolantEnr: 108.50102996826172, 84 | cts: 125.99375915527344, 85 | currentLearn: -28.017152786254883, 86 | dutyCycle: 1.68954598903656, 87 | estimatedVE: 54.11471176147461, 88 | fuelFlow: 8.053908348083496, 89 | fuelPressure: 102.55535888671875, 90 | iacPosition: 6.481048583984375, 91 | ignitionTiming: 4, 92 | injPW: 2.510988473892212, 93 | lambda: 0.7877603769302368, 94 | learnStatus: 0, 95 | map: 32.342926025390625, 96 | mapROC: 0, 97 | mat: 92.84333801269531, 98 | oilPressure: 71.85884857177734, 99 | pedalPosition: 0.08661866933107376, 100 | rpm: 1145.6617431640625, 101 | rtc: 219297876, 102 | targetAFR: 13.799999237060547, 103 | targetIdleSpeed: 985.015625, 104 | targetLambda: 0.9387754797935486, 105 | tps: 1.1150970458984375, 106 | tpsROC: 0, 107 | }); 108 | expect(data.points[163]).toMatchObject({ 109 | afr: 12.163944244384766, 110 | afterstartEnr: 114.87916564941406, 111 | airTempEnr: 102.41293334960938, 112 | baro: 102.9437484741211, 113 | battery: 13.874848365783691, 114 | cLComp: -6.935264587402344, 115 | cLStatus: 1, 116 | coolantEnr: 108.57482147216797, 117 | cts: 125.70072174072266, 118 | currentLearn: -27.968887329101562, 119 | dutyCycle: 1.1940630674362183, 120 | estimatedVE: 49.62789535522461, 121 | fuelFlow: 5.6957221031188965, 122 | fuelPressure: 102.5859375, 123 | iacPosition: 3.92547607421875, 124 | ignitionTiming: 9.938140869140625, 125 | injPW: 2.1353447437286377, 126 | lambda: 0.8274792432785034, 127 | learnStatus: 0, 128 | map: 28.864383697509766, 129 | mapROC: 0, 130 | mat: 91.74858856201172, 131 | oilPressure: 69.73184204101562, 132 | pedalPosition: 0.08433292806148529, 133 | rpm: 1041.2421875, 134 | rtc: 219299136, 135 | targetAFR: 13.799999237060547, 136 | targetIdleSpeed: 985.7481689453125, 137 | targetLambda: 0.9387754797935486, 138 | tps: 0.4452384412288666, 139 | tpsROC: 0, 140 | }); 141 | }); 142 | 143 | test('reads medium datalog', async () => { 144 | // yes I know this is more of an integration test blah blah sue me 145 | const data = await readDatalog('test-data/medium-datalog-0.dl'); 146 | expect(data.tuneFileName).toBe('HELLCAT.terx'); 147 | expect(data.points[0]).toMatchObject({ 148 | afr: 11.992986679077148, 149 | afterstartEnr: 100, 150 | airTempEnr: 102.79972076416016, 151 | baro: 103.08258819580078, 152 | battery: 14.10081672668457, 153 | cLComp: 0, 154 | cLStatus: 0, 155 | coolantEnr: 100, 156 | cts: 202.64935302734375, 157 | currentLearn: -30.10417366027832, 158 | dutyCycle: 1.0028914213180542, 159 | estimatedVE: 40.66032028198242, 160 | fuelFlow: 4.790371894836426, 161 | fuelPressure: 213.6522216796875, 162 | iacPosition: 0, 163 | ignitionTiming: 9.165731430053711, 164 | injPW: 1.6104422807693481, 165 | lambda: 0.8186824321746826, 166 | learnStatus: 0, 167 | map: 22.1372127532959, 168 | mapROC: 0, 169 | mat: 84.01959228515625, 170 | oilPressure: 59.5507926940918, 171 | pedalPosition: 0.07257789373397827, 172 | rpm: 1374.3946533203125, 173 | rtc: 195465854, 174 | targetAFR: 13.843148231506348, 175 | targetIdleSpeed: 850, 176 | targetLambda: 0.9417107701301575, 177 | tps: 0, 178 | tpsROC: 0, 179 | }); 180 | expect(data.points.length).toBe(3791); 181 | expect(data.points[1316]).toMatchObject({ 182 | afr: 12.544939041137695, 183 | afterstartEnr: 100, 184 | airTempEnr: 102.7638931274414, 185 | baro: 102.89029693603516, 186 | battery: 14.299999237060547, 187 | cLStatus: 1, 188 | cLComp: -1.5619462728500366, 189 | coolantEnr: 100, 190 | cts: 203.2503662109375, 191 | currentLearn: -0.5358229279518127, 192 | dutyCycle: 64.91338348388672, 193 | estimatedVE: 100.78258514404297, 194 | fuelFlow: 309.22882080078125, 195 | fuelPressure: 213.8100128173828, 196 | iacPosition: 17, 197 | ignitionTiming: 12, 198 | injPW: 17.173524856567383, 199 | lambda: 0.8532695770263672, 200 | learnStatus: 1, 201 | map: 171.75286865234375, 202 | mapROC: 0, 203 | mat: 84.77455139160156, 204 | oilPressure: 77.22880554199219, 205 | pedalPosition: 62.88169479370117, 206 | rpm: 4733.0439453125, 207 | rtc: 195539194, 208 | targetAFR: 12.5, 209 | targetIdleSpeed: 850, 210 | targetLambda: 0.8503401279449463, 211 | tps: 47.8206901550293, 212 | tpsROC: 0, 213 | }); 214 | expect(data.points[3789]).toMatchObject({ 215 | afr: 13.404864311218262, 216 | afterstartEnr: 100, 217 | airTempEnr: 102.82559204101562, 218 | baro: 103.10332489013672, 219 | battery: 14.085683822631836, 220 | cLComp: 9.784451484680176, 221 | cLStatus: 1, 222 | coolantEnr: 100, 223 | cts: 203.5010223388672, 224 | currentLearn: -20.293493270874023, 225 | dutyCycle: 3.7507967948913574, 226 | estimatedVE: 64.48607635498047, 227 | fuelFlow: 17.93691635131836, 228 | fuelPressure: 209.90426635742188, 229 | iacPosition: 17, 230 | ignitionTiming: 20.049640655517578, 231 | injPW: 3.551645040512085, 232 | lambda: 0.9118955731391907, 233 | learnStatus: 1, 234 | map: 49.916908264160156, 235 | mapROC: 0, 236 | mat: 83.4891357421875, 237 | oilPressure: 62.87053680419922, 238 | pedalPosition: 19.739486694335938, 239 | rpm: 1602.9656982421875, 240 | rtc: 195676814, 241 | targetAFR: 13.638270378112793, 242 | targetIdleSpeed: 850, 243 | targetLambda: 0.9277735352516174, 244 | tps: 6.595186710357666, 245 | tpsROC: 0, 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 305 | 306 | 380 | 381 | 387 | -------------------------------------------------------------------------------- /src/shared/formula.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe } from 'vitest'; 2 | import { 3 | parse, infixToTree, types, evaluate, calculate, 4 | } from './formula'; 5 | 6 | describe('parse', () => { 7 | test('returns nothing', () => { 8 | expect(parse('')).toEqual([]); 9 | }); 10 | 11 | test('returns single digit number', () => { 12 | expect(parse('0')).toEqual([{ type: types.NUMBER, value: 0 }]); 13 | expect(parse('5')).toEqual([{ type: types.NUMBER, value: 5 }]); 14 | expect(parse('9')).toEqual([{ type: types.NUMBER, value: 9 }]); 15 | }); 16 | 17 | test('returns two digit number', () => { 18 | expect(parse('10')).toEqual([{ type: types.NUMBER, value: 10 }]); 19 | expect(parse('90')).toEqual([{ type: types.NUMBER, value: 90 }]); 20 | }); 21 | 22 | test('returns n-digit number', () => { 23 | expect(parse('100')).toEqual([{ type: types.NUMBER, value: 100 }]); 24 | expect(parse('1050')).toEqual([{ type: types.NUMBER, value: 1050 }]); 25 | }); 26 | 27 | test('returns number with decimal places', () => { 28 | expect(parse('1.1')).toEqual([{ type: types.NUMBER, value: 1.1 }]); 29 | expect(parse('.1')).toEqual([{ type: types.NUMBER, value: 0.1 }]); 30 | expect(parse('.0')).toEqual([{ type: types.NUMBER, value: 0 }]); 31 | }); 32 | 33 | test('throws with multiple decimal places', () => { 34 | expect(() => parse('1.1.')).toThrowError(); 35 | expect(() => parse('.1.')).toThrowError(); 36 | expect(() => parse('..1')).toThrowError(); 37 | expect(() => parse('..')).toThrowError(); 38 | expect(() => parse('.')).toThrowError(); 39 | }); 40 | 41 | test('handles whitespace before and after', () => { 42 | expect(parse(' .0')).toEqual([{ type: types.NUMBER, value: 0 }]); 43 | expect(parse('.0 ')).toEqual([{ type: types.NUMBER, value: 0 }]); 44 | expect(parse(' .0 ')).toEqual([{ type: types.NUMBER, value: 0 }]); 45 | }); 46 | 47 | test('returns two numbers', () => { 48 | expect(parse('0 1')).toEqual([ 49 | { type: types.NUMBER, value: 0 }, 50 | { type: types.NUMBER, value: 1 }, 51 | ]); 52 | expect(parse('10.01 5.55')).toEqual([ 53 | { type: types.NUMBER, value: 10.01 }, 54 | { type: types.NUMBER, value: 5.55 }, 55 | ]); 56 | expect(parse(' 10.01 5.55 ')).toEqual([ 57 | { type: types.NUMBER, value: 10.01 }, 58 | { type: types.NUMBER, value: 5.55 }, 59 | ]); 60 | }); 61 | 62 | test('returns n numbers', () => { 63 | expect(parse(' 0 1 232 3.14 ')).toEqual([ 64 | { type: types.NUMBER, value: 0 }, 65 | { type: types.NUMBER, value: 1 }, 66 | { type: types.NUMBER, value: 232 }, 67 | { type: types.NUMBER, value: 3.14 }, 68 | ]); 69 | }); 70 | 71 | test('handles operators +, -, *, /, ^', () => { 72 | expect(parse('+')).toEqual([{ type: types.OPERATOR, value: '+' }]); 73 | expect(parse('-')).toEqual([{ type: types.OPERATOR, value: '-' }]); 74 | expect(parse('*')).toEqual([{ type: types.OPERATOR, value: '*' }]); 75 | expect(parse('/')).toEqual([{ type: types.OPERATOR, value: '/' }]); 76 | expect(parse('^')).toEqual([{ type: types.OPERATOR, value: '^' }]); 77 | }); 78 | 79 | test('handles multiple operators', () => { 80 | expect(parse('++')).toEqual([ 81 | { type: types.OPERATOR, value: '+' }, 82 | { type: types.OPERATOR, value: '+' }, 83 | ]); 84 | expect(parse('- - -')).toEqual([ 85 | { type: types.OPERATOR, value: '-' }, 86 | { type: types.OPERATOR, value: '-' }, 87 | { type: types.OPERATOR, value: '-' }, 88 | ]); 89 | expect(parse('+-*/^')).toEqual([ 90 | { type: types.OPERATOR, value: '+' }, 91 | { type: types.OPERATOR, value: '-' }, 92 | { type: types.OPERATOR, value: '*' }, 93 | { type: types.OPERATOR, value: '/' }, 94 | { type: types.OPERATOR, value: '^' }, 95 | ]); 96 | }); 97 | 98 | test('handles operators and numbers', () => { 99 | expect(parse('5+5')).toEqual([ 100 | { type: types.NUMBER, value: 5 }, 101 | { type: types.OPERATOR, value: '+' }, 102 | { type: types.NUMBER, value: 5 }, 103 | ]); 104 | expect(parse('50+ 0.500--5')).toEqual([ 105 | { type: types.NUMBER, value: 50 }, 106 | { type: types.OPERATOR, value: '+' }, 107 | { type: types.NUMBER, value: 0.5 }, 108 | { type: types.OPERATOR, value: '-' }, 109 | { type: types.OPERATOR, value: '-' }, 110 | { type: types.NUMBER, value: 5 }, 111 | ]); 112 | }); 113 | 114 | test('handles parenthesis', () => { 115 | expect(parse('(')).toEqual([{ type: types.PARENTHESIS, value: '(' }]); 116 | expect(parse(')')).toEqual([{ type: types.PARENTHESIS, value: ')' }]); 117 | }); 118 | 119 | test('handles multiple parentheses', () => { 120 | expect(parse('()')).toEqual([ 121 | { type: types.PARENTHESIS, value: '(' }, 122 | { type: types.PARENTHESIS, value: ')' }, 123 | ]); 124 | expect(parse('(() ()')).toEqual([ 125 | { type: types.PARENTHESIS, value: '(' }, 126 | { type: types.PARENTHESIS, value: '(' }, 127 | { type: types.PARENTHESIS, value: ')' }, 128 | { type: types.PARENTHESIS, value: '(' }, 129 | { type: types.PARENTHESIS, value: ')' }, 130 | ]); 131 | }); 132 | 133 | test('handles parentheses, operators, and numbers', () => { 134 | expect(parse('(5+5)')).toEqual([ 135 | { type: types.PARENTHESIS, value: '(' }, 136 | { type: types.NUMBER, value: 5 }, 137 | { type: types.OPERATOR, value: '+' }, 138 | { type: types.NUMBER, value: 5 }, 139 | { type: types.PARENTHESIS, value: ')' }, 140 | ]); 141 | }); 142 | 143 | test('handles identifier', () => { 144 | expect(parse('var')).toEqual([{ type: types.IDENTIFIER, value: 'var' }]); 145 | expect(parse('var0')).toEqual([{ type: types.IDENTIFIER, value: 'var0' }]); 146 | }); 147 | 148 | test('handles multiple identifiers', () => { 149 | expect(parse('var0 var1')).toEqual([ 150 | { type: types.IDENTIFIER, value: 'var0' }, 151 | { type: types.IDENTIFIER, value: 'var1' }, 152 | ]); 153 | expect(parse('x y z')).toEqual([ 154 | { type: types.IDENTIFIER, value: 'x' }, 155 | { type: types.IDENTIFIER, value: 'y' }, 156 | { type: types.IDENTIFIER, value: 'z' }, 157 | ]); 158 | }); 159 | 160 | test('handles identifiers, numbers, operators, and parentheses', () => { 161 | expect(parse('(var0 + 10)')).toEqual([ 162 | { type: types.PARENTHESIS, value: '(' }, 163 | { type: types.IDENTIFIER, value: 'var0' }, 164 | { type: types.OPERATOR, value: '+' }, 165 | { type: types.NUMBER, value: 10 }, 166 | { type: types.PARENTHESIS, value: ')' }, 167 | ]); 168 | expect(parse('(var0+10*anotherVar/x)')).toEqual([ 169 | { type: types.PARENTHESIS, value: '(' }, 170 | { type: types.IDENTIFIER, value: 'var0' }, 171 | { type: types.OPERATOR, value: '+' }, 172 | { type: types.NUMBER, value: 10 }, 173 | { type: types.OPERATOR, value: '*' }, 174 | { type: types.IDENTIFIER, value: 'anotherVar' }, 175 | { type: types.OPERATOR, value: '/' }, 176 | { type: types.IDENTIFIER, value: 'x' }, 177 | { type: types.PARENTHESIS, value: ')' }, 178 | ]); 179 | }); 180 | 181 | test('handles ambiguous formulae', () => { 182 | expect(parse('5var')).toEqual([ 183 | { type: types.NUMBER, value: 5 }, 184 | { type: types.IDENTIFIER, value: 'var' }, 185 | ]); 186 | expect(parse('var200.5')).toEqual([ 187 | { type: types.IDENTIFIER, value: 'var200' }, 188 | { type: types.NUMBER, value: 0.5 }, 189 | ]); 190 | }); 191 | 192 | test('handles to parse incorrect formulae', () => { 193 | expect(() => parse('var0.var1')).toThrowError('is not a number'); 194 | expect(() => parse('(+.-)')).toThrowError('is not a number'); 195 | }); 196 | }); 197 | 198 | describe('infixToTree', () => { 199 | test('converts nothing', () => { 200 | expect(infixToTree([])).toEqual([]); 201 | }); 202 | 203 | test('converts single number or identifier', () => { 204 | expect(infixToTree([ 205 | { type: types.NUMBER, value: 5 }, 206 | ])).toEqual([ 207 | { type: types.NUMBER, value: 5 }, 208 | ]); 209 | expect(infixToTree([ 210 | { type: types.IDENTIFIER, value: 'e' }, 211 | ])).toEqual([ 212 | { type: types.IDENTIFIER, value: 'e' }, 213 | ]); 214 | }); 215 | 216 | test('converts single simple operation', () => { 217 | expect(infixToTree([ 218 | { type: types.NUMBER, value: 5 }, 219 | { type: types.OPERATOR, value: '*' }, 220 | { type: types.NUMBER, value: 2 }, 221 | ])).toEqual([ 222 | { type: types.OPERATOR, value: '*' }, 223 | { type: types.NUMBER, value: 5 }, 224 | { type: types.NUMBER, value: 2 }, 225 | ]); 226 | }); 227 | 228 | test('converts two operations of same priority', () => { 229 | expect(infixToTree([ 230 | { type: types.NUMBER, value: 2 }, 231 | { type: types.OPERATOR, value: '+' }, 232 | { type: types.NUMBER, value: 3 }, 233 | { type: types.OPERATOR, value: '-' }, 234 | { type: types.NUMBER, value: 4 }, 235 | ])).toEqual([ 236 | { type: types.OPERATOR, value: '-' }, 237 | { type: types.OPERATOR, value: '+' }, 238 | { type: types.NUMBER, value: 4 }, 239 | { type: types.NUMBER, value: 2 }, 240 | { type: types.NUMBER, value: 3 }, 241 | null, null, 242 | ]); 243 | 244 | expect(infixToTree([ 245 | { type: types.NUMBER, value: 2 }, 246 | { type: types.OPERATOR, value: '*' }, 247 | { type: types.NUMBER, value: 3 }, 248 | { type: types.OPERATOR, value: '/' }, 249 | { type: types.NUMBER, value: 4 }, 250 | ])).toEqual([ 251 | { type: types.OPERATOR, value: '/' }, 252 | { type: types.OPERATOR, value: '*' }, 253 | { type: types.NUMBER, value: 4 }, 254 | { type: types.NUMBER, value: 2 }, 255 | { type: types.NUMBER, value: 3 }, 256 | null, null, 257 | ]); 258 | }); 259 | 260 | test('converts two operations of different priority', () => { 261 | expect(infixToTree([ 262 | { type: types.NUMBER, value: 2 }, 263 | { type: types.OPERATOR, value: '+' }, 264 | { type: types.NUMBER, value: 3 }, 265 | { type: types.OPERATOR, value: '*' }, 266 | { type: types.NUMBER, value: 4 }, 267 | ])).toEqual([ 268 | { type: types.OPERATOR, value: '+' }, 269 | { type: types.NUMBER, value: 2 }, 270 | { type: types.OPERATOR, value: '*' }, 271 | null, null, 272 | { type: types.NUMBER, value: 3 }, 273 | { type: types.NUMBER, value: 4 }, 274 | ]); 275 | }); 276 | 277 | test('converts multiple operations of different priority', () => { 278 | expect(infixToTree([ 279 | { type: types.NUMBER, value: 1 }, 280 | { type: types.OPERATOR, value: '+' }, 281 | { type: types.NUMBER, value: 2 }, 282 | { type: types.OPERATOR, value: '*' }, 283 | { type: types.NUMBER, value: 3 }, 284 | { type: types.OPERATOR, value: '/' }, 285 | { type: types.NUMBER, value: 4 }, 286 | { type: types.OPERATOR, value: '+' }, 287 | { type: types.NUMBER, value: 5 }, 288 | { type: types.OPERATOR, value: '*' }, 289 | { type: types.NUMBER, value: 6 }, 290 | ])).toEqual([ 291 | { type: types.OPERATOR, value: '+' }, 292 | { type: types.NUMBER, value: 1 }, 293 | { type: types.OPERATOR, value: '+' }, 294 | null, null, 295 | { type: types.OPERATOR, value: '/' }, 296 | { type: types.OPERATOR, value: '*' }, 297 | null, null, null, null, 298 | { type: types.OPERATOR, value: '*' }, 299 | { type: types.NUMBER, value: 4 }, 300 | { type: types.NUMBER, value: 5 }, 301 | { type: types.NUMBER, value: 6 }, 302 | null, null, null, null, null, null, null, null, 303 | { type: types.NUMBER, value: 2 }, 304 | { type: types.NUMBER, value: 3 }, 305 | null, null, null, null, null, null, 306 | ]); 307 | }); 308 | 309 | test('converts with operators and parentheses', () => { 310 | expect(infixToTree([ 311 | { type: types.NUMBER, value: 1 }, 312 | { type: types.OPERATOR, value: '*' }, 313 | { type: types.PARENTHESIS, value: '(' }, 314 | { type: types.NUMBER, value: 2 }, 315 | { type: types.OPERATOR, value: '+' }, 316 | { type: types.NUMBER, value: 3 }, 317 | { type: types.PARENTHESIS, value: ')' }, 318 | ])).toEqual([ 319 | { type: types.OPERATOR, value: '*' }, 320 | { type: types.NUMBER, value: 1 }, 321 | { type: types.PARENTHESIS, value: '(' }, 322 | null, null, 323 | { type: types.NOOP }, 324 | { type: types.OPERATOR, value: '+' }, 325 | null, null, null, null, null, null, 326 | { type: types.NUMBER, value: 2 }, 327 | { type: types.NUMBER, value: 3 }, 328 | ]); 329 | 330 | expect(infixToTree([ 331 | { type: types.PARENTHESIS, value: '(' }, 332 | { type: types.NUMBER, value: 1 }, 333 | { type: types.OPERATOR, value: '+' }, 334 | { type: types.NUMBER, value: 2 }, 335 | { type: types.PARENTHESIS, value: ')' }, 336 | ])).toEqual([ 337 | { type: types.PARENTHESIS, value: '(' }, 338 | { type: types.NOOP }, 339 | { type: types.OPERATOR, value: '+' }, 340 | null, null, 341 | { type: types.NUMBER, value: 1 }, 342 | { type: types.NUMBER, value: 2 }, 343 | ]); 344 | 345 | expect(infixToTree([ 346 | { type: types.NUMBER, value: 1 }, 347 | { type: types.PARENTHESIS, value: '(' }, 348 | { type: types.NUMBER, value: 2 }, 349 | { type: types.OPERATOR, value: '+' }, 350 | { type: types.NUMBER, value: 3 }, 351 | { type: types.PARENTHESIS, value: ')' }, 352 | ])).toEqual([ 353 | { type: types.PARENTHESIS, value: '(' }, 354 | { type: types.NUMBER, value: 1 }, 355 | { type: types.OPERATOR, value: '+' }, 356 | null, null, 357 | { type: types.NUMBER, value: 2 }, 358 | { type: types.NUMBER, value: 3 }, 359 | ]); 360 | 361 | expect(infixToTree([ 362 | { type: types.NUMBER, value: 1 }, 363 | { type: types.OPERATOR, value: '+' }, 364 | { type: types.NUMBER, value: 2 }, 365 | { type: types.PARENTHESIS, value: '(' }, 366 | { type: types.NUMBER, value: 3 }, 367 | { type: types.OPERATOR, value: '+' }, 368 | { type: types.NUMBER, value: 4 }, 369 | { type: types.PARENTHESIS, value: ')' }, 370 | ])).toEqual([ 371 | { type: types.OPERATOR, value: '+' }, 372 | { type: types.NUMBER, value: 1 }, 373 | { type: types.PARENTHESIS, value: '(' }, 374 | null, null, 375 | { type: types.NUMBER, value: 2 }, 376 | { type: types.OPERATOR, value: '+' }, 377 | null, null, null, null, null, null, 378 | { type: types.NUMBER, value: 3 }, 379 | { type: types.NUMBER, value: 4 }, 380 | ]); 381 | 382 | expect(infixToTree([ 383 | { type: types.PARENTHESIS, value: '(' }, 384 | { type: types.NUMBER, value: 1 }, 385 | { type: types.OPERATOR, value: '+' }, 386 | { type: types.NUMBER, value: 3 }, 387 | { type: types.PARENTHESIS, value: ')' }, 388 | { type: types.OPERATOR, value: '*' }, 389 | { type: types.NUMBER, value: 2 }, 390 | ])).toEqual([ 391 | { type: types.OPERATOR, value: '*' }, 392 | { type: types.PARENTHESIS, value: '(' }, 393 | { type: types.NUMBER, value: 2 }, 394 | { type: types.NOOP }, 395 | { type: types.OPERATOR, value: '+' }, 396 | null, null, null, null, 397 | { type: types.NUMBER, value: 1 }, 398 | { type: types.NUMBER, value: 3 }, 399 | null, null, null, null, 400 | ]); 401 | }); 402 | 403 | test('converts with multiple sets of parentheses', () => { 404 | expect(infixToTree([ 405 | { type: types.PARENTHESIS, value: '(' }, 406 | { type: types.NUMBER, value: 1 }, 407 | { type: types.OPERATOR, value: '+' }, 408 | { type: types.PARENTHESIS, value: '(' }, 409 | { type: types.NUMBER, value: 2 }, 410 | { type: types.OPERATOR, value: '+' }, 411 | { type: types.NUMBER, value: 3 }, 412 | { type: types.PARENTHESIS, value: ')' }, 413 | { type: types.PARENTHESIS, value: ')' }, 414 | ])).toEqual([ 415 | { type: types.PARENTHESIS, value: '(' }, 416 | { type: types.NOOP }, 417 | { type: types.OPERATOR, value: '+' }, 418 | null, null, 419 | { type: types.NUMBER, value: 1 }, 420 | { type: types.PARENTHESIS, value: '(' }, 421 | null, null, null, null, null, null, 422 | { type: types.NOOP }, 423 | { type: types.OPERATOR, value: '+' }, 424 | null, null, null, null, null, null, null, null, null, null, null, null, null, null, 425 | { type: types.NUMBER, value: 2 }, 426 | { type: types.NUMBER, value: 3 }, 427 | ]); 428 | 429 | expect(infixToTree([ 430 | { type: types.PARENTHESIS, value: '(' }, 431 | { type: types.NUMBER, value: 1 }, 432 | { type: types.PARENTHESIS, value: ')' }, 433 | { type: types.PARENTHESIS, value: '(' }, 434 | { type: types.NUMBER, value: 2 }, 435 | { type: types.PARENTHESIS, value: ')' }, 436 | ])).toEqual([ 437 | { type: types.PARENTHESIS, value: '(' }, 438 | { type: types.PARENTHESIS, value: '(' }, 439 | { type: types.NUMBER, value: 2 }, 440 | { type: types.NOOP }, 441 | { type: types.NUMBER, value: 1 }, 442 | null, null, 443 | ]); 444 | }); 445 | 446 | test('converts with exponents', () => { 447 | expect(infixToTree([ 448 | { type: types.NUMBER, value: 1 }, 449 | { type: types.OPERATOR, value: '^' }, 450 | { type: types.NUMBER, value: 2 }, 451 | ])).toEqual([ 452 | { type: types.OPERATOR, value: '^' }, 453 | { type: types.NUMBER, value: 1 }, 454 | { type: types.NUMBER, value: 2 }, 455 | ]); 456 | 457 | expect(infixToTree([ 458 | { type: types.NUMBER, value: 1 }, 459 | { type: types.OPERATOR, value: '^' }, 460 | { type: types.NUMBER, value: 2 }, 461 | { type: types.OPERATOR, value: '*' }, 462 | { type: types.NUMBER, value: 3 }, 463 | ])).toEqual([ 464 | { type: types.OPERATOR, value: '*' }, 465 | { type: types.OPERATOR, value: '^' }, 466 | { type: types.NUMBER, value: 3 }, 467 | { type: types.NUMBER, value: 1 }, 468 | { type: types.NUMBER, value: 2 }, 469 | null, null, 470 | ]); 471 | 472 | expect(infixToTree([ 473 | { type: types.NUMBER, value: 1 }, 474 | { type: types.OPERATOR, value: '^' }, 475 | { type: types.NUMBER, value: 2 }, 476 | { type: types.OPERATOR, value: '^' }, 477 | { type: types.NUMBER, value: 3 }, 478 | ])).toEqual([ 479 | { type: types.OPERATOR, value: '^' }, 480 | { type: types.OPERATOR, value: '^' }, 481 | { type: types.NUMBER, value: 3 }, 482 | { type: types.NUMBER, value: 1 }, 483 | { type: types.NUMBER, value: 2 }, 484 | null, null, 485 | ]); 486 | 487 | expect(infixToTree([ 488 | { type: types.NUMBER, value: 1 }, 489 | { type: types.OPERATOR, value: '^' }, 490 | { type: types.PARENTHESIS, value: '(' }, 491 | { type: types.NUMBER, value: 2 }, 492 | { type: types.OPERATOR, value: '+' }, 493 | { type: types.NUMBER, value: 3 }, 494 | { type: types.PARENTHESIS, value: ')' }, 495 | ])).toEqual([ 496 | { type: types.OPERATOR, value: '^' }, 497 | { type: types.NUMBER, value: 1 }, 498 | { type: types.PARENTHESIS, value: '(' }, 499 | null, null, 500 | { type: types.NOOP }, 501 | { type: types.OPERATOR, value: '+' }, 502 | null, null, null, null, null, null, 503 | { type: types.NUMBER, value: 2 }, 504 | { type: types.NUMBER, value: 3 }, 505 | ]); 506 | 507 | expect(infixToTree([ 508 | { type: types.PARENTHESIS, value: '(' }, 509 | { type: types.NUMBER, value: 3 }, 510 | { type: types.OPERATOR, value: '^' }, 511 | { type: types.NUMBER, value: 2 }, 512 | { type: types.OPERATOR, value: '+' }, 513 | { type: types.NUMBER, value: 4 }, 514 | { type: types.OPERATOR, value: '^' }, 515 | { type: types.NUMBER, value: 2 }, 516 | { type: types.PARENTHESIS, value: ')' }, 517 | { type: types.OPERATOR, value: '^' }, 518 | { type: types.NUMBER, value: 0.5 }, 519 | ])).toEqual([ 520 | { type: types.OPERATOR, value: '^' }, 521 | { type: types.PARENTHESIS, value: '(' }, 522 | { type: types.NUMBER, value: 0.5 }, 523 | { type: types.NOOP }, 524 | { type: types.OPERATOR, value: '+' }, 525 | null, null, null, null, 526 | { type: types.OPERATOR, value: '^' }, 527 | { type: types.OPERATOR, value: '^' }, 528 | null, null, null, null, null, null, null, null, 529 | { type: types.NUMBER, value: 3 }, 530 | { type: types.NUMBER, value: 2 }, 531 | { type: types.NUMBER, value: 4 }, 532 | { type: types.NUMBER, value: 2 }, 533 | null, null, null, null, null, null, null, null, 534 | ]); 535 | }); 536 | 537 | test('converts edge cases', () => { 538 | expect(infixToTree([ 539 | { type: types.OPERATOR, value: '-' }, 540 | { type: types.IDENTIFIER, value: 'b' }, 541 | { type: types.OPERATOR, value: '+' }, 542 | { type: types.NUMBER, value: 2 }, 543 | ])).toEqual([ 544 | { type: types.OPERATOR, value: '+' }, 545 | { type: types.OPERATOR, value: '-' }, 546 | { type: types.NUMBER, value: 2 }, 547 | { type: types.NOOP }, 548 | { type: types.IDENTIFIER, value: 'b' }, 549 | null, null, 550 | ]); 551 | }); 552 | }); 553 | 554 | describe('evaluate', () => { 555 | test('computes single number', () => { 556 | const tree = [ 557 | { type: types.NUMBER, value: 5 }, 558 | ]; 559 | expect(evaluate(tree, {})).toBe(5); 560 | }); 561 | 562 | test('computes single identifier', () => { 563 | const tree = [ 564 | { type: types.IDENTIFIER, value: 'b' }, 565 | ]; 566 | expect(evaluate(tree, { b: 2 })).toBe(2); 567 | }); 568 | 569 | test('computes 1+2', () => { 570 | const tree = [ 571 | { type: types.OPERATOR, value: '+' }, 572 | { type: types.NUMBER, value: 1 }, 573 | { type: types.NUMBER, value: 2 }, 574 | ]; 575 | expect(evaluate(tree, {})).toBe(3); 576 | }); 577 | 578 | test('computes negative', () => { 579 | const tree = [ 580 | { type: types.OPERATOR, value: '-' }, 581 | { type: types.NUMBER, value: 1 }, 582 | null, 583 | ]; 584 | expect(evaluate(tree, {})).toBe(-1); 585 | }); 586 | 587 | test('computes 2+3-4', () => { 588 | const tree = [ 589 | { type: types.OPERATOR, value: '-' }, 590 | { type: types.OPERATOR, value: '+' }, 591 | { type: types.NUMBER, value: 4 }, 592 | { type: types.NUMBER, value: 2 }, 593 | { type: types.NUMBER, value: 3 }, 594 | ]; 595 | expect(evaluate(tree, {})).toBe(1); 596 | }); 597 | 598 | test('computes 2*3/4', () => { 599 | const tree = [ 600 | { type: types.OPERATOR, value: '/' }, 601 | { type: types.OPERATOR, value: '*' }, 602 | { type: types.NUMBER, value: 4 }, 603 | { type: types.NUMBER, value: 2 }, 604 | { type: types.NUMBER, value: 3 }, 605 | ]; 606 | expect(evaluate(tree, {})).toBe(1.5); 607 | }); 608 | 609 | test('computes 2+3*4', () => { 610 | const tree = [ 611 | { type: types.OPERATOR, value: '+' }, 612 | { type: types.NUMBER, value: 2 }, 613 | { type: types.OPERATOR, value: '*' }, 614 | null, null, 615 | { type: types.NUMBER, value: 3 }, 616 | { type: types.NUMBER, value: 4 }, 617 | ]; 618 | expect(evaluate(tree, {})).toBe(14); 619 | }); 620 | 621 | test('computes 1+2*3/4+5*6', () => { 622 | const tree = [ 623 | { type: types.OPERATOR, value: '+' }, 624 | { type: types.NUMBER, value: 1 }, 625 | { type: types.OPERATOR, value: '+' }, 626 | null, null, 627 | { type: types.OPERATOR, value: '/' }, 628 | { type: types.OPERATOR, value: '*' }, 629 | null, null, null, null, 630 | { type: types.OPERATOR, value: '*' }, 631 | { type: types.NUMBER, value: 4 }, 632 | { type: types.NUMBER, value: 5 }, 633 | { type: types.NUMBER, value: 6 }, 634 | null, null, null, null, null, null, null, null, 635 | { type: types.NUMBER, value: 2 }, 636 | { type: types.NUMBER, value: 3 }, 637 | ]; 638 | expect(evaluate(tree, {})).toBe(32.5); 639 | }); 640 | 641 | test('computes 5*(2+3)', () => { 642 | const tree = [ 643 | { type: types.OPERATOR, value: '*' }, 644 | { type: types.NUMBER, value: 5 }, 645 | { type: types.PARENTHESIS, value: '(' }, 646 | null, null, null, 647 | { type: types.OPERATOR, value: '+' }, 648 | null, null, null, null, null, null, 649 | { type: types.NUMBER, value: 2 }, 650 | { type: types.NUMBER, value: 3 }, 651 | ]; 652 | expect(evaluate(tree, {})).toBe(25); 653 | }); 654 | 655 | test('computes (1+2)', () => { 656 | const tree = [ 657 | { type: types.PARENTHESIS, value: '(' }, 658 | null, 659 | { type: types.OPERATOR, value: '+' }, 660 | null, null, 661 | { type: types.NUMBER, value: 1 }, 662 | { type: types.NUMBER, value: 2 }, 663 | ]; 664 | expect(evaluate(tree, {})).toBe(3); 665 | }); 666 | 667 | test('computes 5(2+3)', () => { 668 | const tree = [ 669 | { type: types.PARENTHESIS, value: '(' }, 670 | { type: types.NUMBER, value: 5 }, 671 | { type: types.OPERATOR, value: '+' }, 672 | null, null, 673 | { type: types.NUMBER, value: 2 }, 674 | { type: types.NUMBER, value: 3 }, 675 | ]; 676 | expect(evaluate(tree, {})).toBe(25); 677 | }); 678 | 679 | test('computes 1+2(3+4)', () => { 680 | const tree = [ 681 | { type: types.OPERATOR, value: '+' }, 682 | { type: types.NUMBER, value: 1 }, 683 | { type: types.PARENTHESIS, value: '(' }, 684 | null, null, 685 | { type: types.NUMBER, value: 2 }, 686 | { type: types.OPERATOR, value: '+' }, 687 | null, null, null, null, null, null, 688 | { type: types.NUMBER, value: 3 }, 689 | { type: types.NUMBER, value: 4 }, 690 | ]; 691 | expect(evaluate(tree, {})).toBe(15); 692 | }); 693 | 694 | test('computes (3)(2)', () => { 695 | const tree = [ 696 | { type: types.PARENTHESIS, value: '(' }, 697 | { type: types.PARENTHESIS, value: '(' }, 698 | { type: types.NUMBER, value: 2 }, 699 | null, 700 | { type: types.NUMBER, value: 3 }, 701 | ]; 702 | expect(evaluate(tree, {})).toBe(6); 703 | }); 704 | 705 | test('computes 5^2*3', () => { 706 | const tree = [ 707 | { type: types.OPERATOR, value: '*' }, 708 | { type: types.OPERATOR, value: '^' }, 709 | { type: types.NUMBER, value: 3 }, 710 | { type: types.NUMBER, value: 5 }, 711 | { type: types.NUMBER, value: 2 }, 712 | ]; 713 | expect(evaluate(tree, {})).toBe(75); 714 | }); 715 | 716 | test('computes 5^(2+3)', () => { 717 | const tree = [ 718 | { type: types.OPERATOR, value: '^' }, 719 | { type: types.NUMBER, value: 5 }, 720 | { type: types.PARENTHESIS, value: '(' }, 721 | null, null, null, 722 | { type: types.OPERATOR, value: '+' }, 723 | null, null, null, null, null, null, 724 | { type: types.NUMBER, value: 2 }, 725 | { type: types.NUMBER, value: 3 }, 726 | ]; 727 | expect(evaluate(tree, {})).toBe(3125); 728 | }); 729 | 730 | test('computes -5+2', () => { 731 | const tree = [ 732 | { type: types.OPERATOR, value: '+' }, 733 | { type: types.OPERATOR, value: '-' }, 734 | { type: types.NUMBER, value: 2 }, 735 | null, 736 | { type: types.NUMBER, value: 5 }, 737 | ]; 738 | expect(evaluate(tree, {})).toBe(-3); 739 | }); 740 | }); 741 | 742 | describe('calculate', () => { 743 | test('calculates pythagorean theorem', () => { 744 | expect(calculate('(a^2+b^2)^(1/2)', { a: 3, b: 4 })).toBeCloseTo(5); 745 | }); 746 | 747 | test('calculates quadratic formula', () => { 748 | expect(calculate('(-b+(b^2-4*a*c)^(1/2))/(2*a)', { a: 5, b: 13, c: 7 })).toBeCloseTo(-0.76148, 4); 749 | expect(calculate('(-b-(b^2-4*a*c)^(1/2))/(2*a)', { a: 5, b: 13, c: 7 })).toBeCloseTo(-1.83851, 4); 750 | }); 751 | 752 | test('calculates gravitational pull', () => { 753 | // Earth 754 | expect(calculate('G * m0 * m1 / (r^2)', { 755 | G: 6.6743E-11, m0: 5.972E24, m1: 1, r: 6.371E6, 756 | })).toBeCloseTo(9.81997, 4); 757 | // Saturn 758 | expect(calculate('G * m0 * m1 / (r^2)', { 759 | G: 6.6743E-11, m0: 5.6834E26, m1: 1, r: 5.8232E7, 760 | })).toBeCloseTo(11.18640, 4); 761 | }); 762 | }); 763 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | 676 | --------------------------------------------------------------------------------