This commit is contained in:
ゆめ 2023-07-06 04:42:03 -05:00
commit d3d2327112
39 changed files with 3612 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/Cargo.lock

24
Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "unitdc"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.19"
num-bigint = { version = "0.4.3", features = ["serde"] }
num-rational = { version = "0.4.1", features = ["serde"] }
num-traits = "0.2.15"
ouroboros = "0.17.0"
serde = { version = "1.0.164", features = ["derive"] }
thiserror = "1.0.40"
[workspace]
members = [
"crates/unitdc-cli",
"crates/unitdc-web",
]
[profile.release.package.unitdc-web]
opt-level = "s"

View file

@ -0,0 +1,9 @@
[package]
name = "unitdc-cli"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
unitdc = { path = "../../" }

View file

@ -0,0 +1,25 @@
use unitdc::interpreter::{Interpreter, Output};
fn main() {
let mut interpreter = Interpreter::new(Box::new(|output| match output {
Output::Message(e) => eprintln!("message: {}", e),
Output::Quantity(q) => println!("[0]: {}", q),
Output::QuantityList(mut q) => {
q.reverse();
for (i, q) in q.iter().enumerate() {
println!("[{}]: {}", i, q)
}
}
}));
interpreter
.run_str(include_str!("../../../unitdc.rc"))
.expect("unitdc.rc should run");
for line in std::io::stdin().lines() {
let line = line.expect("line should exist");
if let Err(e) = interpreter.run_str(&line) {
eprintln!("{}", e)
}
}
}

6
crates/unitdc-web/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/target
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log

View file

@ -0,0 +1,34 @@
[package]
name = "unitdc-web"
version = "0.1.0"
authors = ["eternal-flame-AD <yume@yumechi.jp>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.63"
unitdc = { path = "../../" }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.6", optional = true }
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
wee_alloc = { version = "0.4.5", optional = true }
serde_json = "1.0.99"
web-sys = { version = "0.3.64", features = ["Window"] }
serde-wasm-bindgen = "0.5.0"
js-sys = "0.3.64"
console_log = { version = "1.0.0", features = ["wasm-bindgen"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.13"

View file

@ -0,0 +1,75 @@
mod utils;
use js_sys::Function;
use unitdc::interpreter::{Interpreter, Output};
use utils::set_panic_hook;
use wasm_bindgen::prelude::*;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
static mut INTERPRETER: Option<Interpreter> = None;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: String);
}
#[wasm_bindgen]
pub fn unitdc_input(input: String) -> Result<(), JsValue> {
unsafe {
if let Some(ref mut interpreter) = INTERPRETER {
interpreter.run_str(&input).map_err(|e| e.to_string())?;
}
}
Ok(())
}
#[wasm_bindgen]
pub fn unitdc_init(js_output: Function) {
unsafe {
INTERPRETER = Some(Interpreter::new(Box::new(move |output| match output {
Output::Quantity(q) => {
js_output
.call2(
&JsValue::NULL,
&JsValue::from("quantity"),
&serde_wasm_bindgen::to_value(&q).unwrap(),
)
.unwrap();
}
Output::QuantityList(q) => {
js_output
.call2(
&JsValue::NULL,
&JsValue::from("quantity_list"),
&serde_wasm_bindgen::to_value(&q).unwrap(),
)
.unwrap();
}
Output::Message(e) => {
js_output
.call2(&JsValue::NULL, &JsValue::from("message"), &JsValue::from(e))
.unwrap();
}
})));
INTERPRETER
.as_mut()
.unwrap()
.run_str(include_str!("../../../unitdc.rc"))
.unwrap();
}
}
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
#[cfg(feature = "console_log")]
console_log::init().expect("could not initialize logger");
set_panic_hook();
Ok(())
}

View file

@ -0,0 +1,10 @@
pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}

View file

@ -0,0 +1,14 @@
module.exports = {
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',
},
}

24
crates/unitdc-web/ui/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,28 @@
{
"name": "ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"typescript": "^5.0.2",
"vite": "^4.3.9"
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,123 @@
html {
height: 100%;
}
.unitdc-io {
margin: .5em .25em .5em;
min-height: 1em;
font-size: 20;
padding: .25rem .25rem .5rem;
}
#unitdc-dialog {
max-height: calc(60vh);
padding-bottom: .5rem;
overflow-y: scroll;
overflow-x: visible;
}
.unitdc-container {
overflow-y: hidden;
position: relative;
height: 100%;
}
label span.submit-hint {
font-style: italic;
font-size: smaller;
float: right;
color: #808080;
display: none;
}
.input-active label span.submit-hint {
display: block;
}
.input {
background-color: #ececec;
white-space: pre;
}
.output {
background-color: lightgreen;
white-space: pre;
}
.error {
background-color: lightpink;
white-space: pre;
}
.input textarea {
display: block;
width: 80%;
overflow: hidden;
resize: none;
background-color: rgba(0, 0, 0, 0);
border: none;
outline: none;
padding-left: 2em;
}
.input input[type=button] {
width: 100%;
}
.unitdc-keyboard {
padding: .25rem .25rem .5rem;
position: absolute;
bottom: 0px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
background-color: #efefef;
width: 100%;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.unitdc-keyboard-spacer {
height: calc(18vh);
width: 100%;
display: block;
}
.unitdc-keyboard .keyboard-key {
font-size: calc(2vh);
padding: calc(.5vh);
height: calc(3vh);
font-weight: bolder;
text-align: center;
background-color: #ffffff;
margin: .15em;
}
.keyboard-key:hover {
background-color: #efefef;
}
.keyboard-key:active {
background-color: gray;
}
.unitdc-keyboard .modifier-pressed {
filter: brightness(.6);
}
div.keyboard-key[data-tokentype=operator] {
background-color: pink;
}
div.keyboard-key[data-tokentype=literal_num] {
background-color: lightskyblue;
}
div.keyboard-key[data-tokentype=unit_modifier] {
background-color: ivory;
}
div.keyboard-key[data-tokentype=unit] {
background-color: lightsteelblue;
}

View file

@ -0,0 +1,225 @@
import { useState, useReducer } from 'react'
import './App.css'
import { Keyboard, TokenType } from './components/Keyboard'
import { InputCell } from './components/InputCell';
import unitdc_wasm, { unitdc_input, unitdc_init } from '../../pkg';
import { ErrorCell } from './components/ErrorCell';
import { Quantity } from './types';
import { OutputCell } from './components/OutputCell';
import { useForceUpdate } from './util';
type IoCellDef = IOTextCellDef | IOQuantityCellDef;
interface IOQuantityCellDef {
type: 'output',
quantity: Quantity[]
}
interface IOTextCellDef {
type: 'input' | 'message' | 'error',
text: string,
}
type IoCellAction = IoCellAddAction | IoCellUpdateTextAction;
interface IoCellAddAction {
type: 'add',
cell: IoCellDef,
}
interface IoCellUpdateTextAction {
type: 'updateText',
index: number,
text: string,
}
function IoCellReducer(state: IoCellDef[], action: IoCellAction) {
switch (action.type) {
case 'add':
return [
...state,
action.cell,
]
case 'updateText':
const newState = [...state];
(newState[action.index] as IOTextCellDef).text = action.text;
return newState;
default:
throw new Error();
}
}
function App() {
const [ioCells, ioCellsDispatch] = useReducer(IoCellReducer, []);
const [mounted, setMounted] = useState(false);
const [lastTokenType, setLastTokenType] = useState<TokenType | "">("");
const forceUpdate = useForceUpdate();
const addCells = (cells: IoCellDef[]) => {
cells.forEach((cell) => {
ioCellsDispatch({ type: 'add', cell: cell })
})
}
const lastInputCell = () => ioCells.filter((cell) => cell.type === 'input').slice(-1)[0] as IOTextCellDef;
const appendToken = (token: string, tokenType: TokenType) => {
let cell = lastInputCell();
if (lastTokenType != tokenType) {
cell.text += ' ';
}
if (tokenType === 'unit') {
token = '(' + token + ')';
}
cell.text += token;
setLastTokenType(tokenType);
forceUpdate();
}
const processOutput = (type: 'quantity' | 'quantity_list' | 'message', data: any) => {
if (type === 'quantity') {
addCells([
{
type: 'output',
quantity: [data],
}
])
} else if (type === 'quantity_list') {
addCells([
{
type: 'output',
quantity: data,
}
])
} else if (type === 'message') {
addCells([
{
type: 'message',
text: data,
}
])
}
}
const submit = () => {
let success = false;
let text = lastInputCell().text;
console.log('submit', text);
try {
unitdc_input(text)
success = true;
} catch (e) {
addCells([
{
type: 'error',
text: (e as any).toString(),
}
])
}
addCells([
{
type: 'input',
text: success ? '' : text,
}
])
};
if (!mounted) {
setMounted(true);
unitdc_wasm().then(() => {
unitdc_init(processOutput);
addCells([
{
type: 'input',
text: '',
}
])
});
}
return (
<>
<div className="unitdc-container">
<h1 style={{ whiteSpace: 'nowrap' }}>
UnitDC
<span style={{
fontStyle: 'italic',
fontWeight: 'lighter',
fontSize: 'smaller',
}} id="unitdc-description">Unit-aware Desk Calculator</span>
</h1>
<div id="unitdc-dialog">
{
ioCells.map((cell, index) => {
switch (cell.type) {
case 'output':
return (
<OutputCell quantities={cell.quantity} key={index} />
)
case 'input':
return (
<InputCell
key={index} number={index}
text={cell.text} active={index == ioCells.length - 1}
onchange={(value) => {
console.log('onchange', value);
ioCellsDispatch({ type: 'updateText', index: index, text: value })
}}
onsubmit={submit}
/>
)
case 'error':
return (
<ErrorCell key={index} text={cell.text} />
)
case 'message':
return (
<div key={index}>Message: {cell.text}</div>
)
}
})
}
</div>
{
<div className="unitdc-keyboard-spacer"></div>
}
</div>
{
<Keyboard
onToken={appendToken}
onUiAction={(action) => {
switch (action) {
case 'append_space':
lastInputCell().text += ' ';
forceUpdate();
break;
case 'append_newline':
lastInputCell().text += '\n';
forceUpdate();
break;
case 'clear':
lastInputCell().text = '';
forceUpdate();
break;
case 'backspace':
lastInputCell().text = lastInputCell().text.slice(0, -1);
forceUpdate();
break;
case 'submit':
submit();
break;
}
}}
/>
}
</>
)
}
export default App

View file

@ -0,0 +1,12 @@
export interface ErrorCellProps {
text: string
}
export function ErrorCell(props: ErrorCellProps) {
return (
<div className="unitdc-io error">
<label className="prompt">Error:</label>
<div className="error-text">{props.text}</div>
</div>
)
}

View file

@ -0,0 +1,58 @@
import { useEffect, useRef } from "react"
export interface InputCellProps {
number: number,
active?: boolean
text: string
onchange?: (value: string) => void
onsubmit?: (value: string) => void
}
export function InputCell(props: InputCellProps) {
const ref = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Enter" && event.shiftKey) {
event.preventDefault()
if (props.onsubmit) {
props.onsubmit(ref.current?.value || "")
}
}
}
if (props.active) {
ref.current?.focus()
document.addEventListener("keydown", handleKeyDown)
} else {
document.removeEventListener("keydown", handleKeyDown)
}
return () => {
document.removeEventListener("keydown", handleKeyDown)
}
}, [props.active])
const onInput = () => {
ref.current!.style.height = "auto"
ref.current!.style.height = ref.current!.scrollHeight + "px"
if (props.onchange) {
props.onchange(ref.current?.value || "")
}
}
useEffect(() => {
ref.current!.value = props.text
ref.current!.addEventListener("input", onInput)
return () => {
ref.current!.removeEventListener("input", onInput)
}
}, [props.text])
return (
<div className={"unitdc-io input" + (props.active ? " input-active" : "")}>
<label className="prompt">{`In [${props.number}]:`} <span className="submit-hint">Shift-Enter to Submit</span></label>
<textarea className="input-text" ref={ref} readOnly={!props.active} />
</div>
)
}

View file

@ -0,0 +1,140 @@
import { useRef, useState } from "react"
export type TokenType = "operator" | "literal_num" | "unit"
export type UiAction = "append_space" | "append_newline" | "backspace" | "submit" | "clear"
export interface KeyboardProps {
onUiAction: (action: UiAction) => void
onToken: (token: string, tokentype: TokenType) => void
}
export function Keyboard(props: KeyboardProps) {
let ref = useRef<HTMLDivElement>(null)
let [mounted, setMounted] = useState(false);
let [modifier_pressed, setModifierPressed] = useState("");
if (!mounted) {
setMounted(true);
}
function ModifierButton(props: { modifier: string }) {
return (
<div
className={"keyboard-key" + (modifier_pressed == props.modifier ? " modifier-pressed" : "")}
data-tokentype="unit_modifier"
onClick={() => setModifierPressed(modifier_pressed == props.modifier ? "" : props.modifier)}>{`(${props.modifier}*)`}</div>
)
}
function UiActionButton(btn_props: { action: UiAction, text: string }) {
return (
<div className="keyboard-key" onClick={() => { props.onUiAction(btn_props.action) }}>{btn_props.text}</div>
)
}
function TokenButton(btn_props: { token: string, tokentype: TokenType, text?: string }) {
let token = btn_props.token;
if (btn_props.tokentype == "unit") {
token = modifier_pressed + btn_props.token;
}
return (
<div className="keyboard-key" data-tokentype={btn_props.tokentype}
onClick={() => {
setModifierPressed("")
props.onToken(token, btn_props.tokentype)
}}>{btn_props.text || btn_props.token}</div>
)
}
return (
<div className="unitdc-keyboard" id="unitdc-keyboard" ref={ref}>
<div className="keyboard-col">
<div className="keyboard-key" data-tokentype="operator">c</div>
{
["k", "c", "d"].map((modifier) => {
return (
<ModifierButton modifier={modifier} key={modifier} />
)
})
}
<UiActionButton action="append_space" text="␣" />
</div>
<div className="keyboard-col">
<div className="keyboard-key" data-tokentype="operator">d</div>
{
["m", "u", "n"].map((modifier) => {
return (
<ModifierButton modifier={modifier} key={modifier} />
)
})
}
<UiActionButton action="backspace" text="←" />
</div>
<div className="keyboard-col">
<TokenButton token="v" tokentype="operator" />
{
["7", "4", "1", "."].map((token) => {
return (
<TokenButton token={token} tokentype="literal_num" key={token} />
)
})
}
</div>
<div className="keyboard-col">
<TokenButton token="p" tokentype="operator" />
{
["8", "5", "2", "0"].map((token) => {
return (
<TokenButton token={token} tokentype="literal_num" key={token} />
)
})
}
</div>
<div className="keyboard-col">
<TokenButton token="n" tokentype="operator" />
{
["9", "6", "3", "e"].map((token) => {
return (
<TokenButton token={token} tokentype="literal_num" key={token} />
)
})
}
<UiActionButton action="append_newline" text="↩" />
</div>
<div className="keyboard-col">
{
["f", "+", "-", "*", "/"].map((token) => {
return (
<TokenButton token={token} tokentype="operator" key={token} />
)
})
}
<UiActionButton action="submit" text="✓" />
</div>
<div className="keyboard-col">
<TokenButton token="r" tokentype="operator" />
<div className="keyboard-key" data-tokentype="operator">r</div>
{
["1", "g", "l", "iu"].map((token) => {
return (
<TokenButton token={token} tokentype="unit" text={`(${token})`} key={token} />
)
})
}
</div>
<div className="keyboard-col">
<UiActionButton action="clear" text="CLR" />
{
["m", "mol", "M", "Da"].map((token) => {
return (
<TokenButton token={token} tokentype="unit" text={`(${token})`} key={token} />
)
})
}
</div>
</div>
)
}

View file

@ -0,0 +1,16 @@
import { Quantity } from "../types";
export interface OutputCellProps {
quantities: Quantity[]
}
export function OutputCell(props: OutputCellProps) {
return (
<div className="unitdc-io output">
<label className="prompt">Out:</label>
<div className="output-text" style={{ paddingLeft: '2em' }}>
{props.quantities.map((q, index) => `[${props.quantities.length - 1 - index}]: ${q._str}`).reverse().join("\r\n")}
</div>
</div>
)
}

View file

@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View file

@ -0,0 +1,16 @@
export interface Quantity {
_str: string;
number_float: number;
unit: UnitCombo,
}
export type UnitCombo = UnitExponent[];
export interface UnitExponent {
unit: string;
exponent: number;
}
export interface Unit {
symbol: string;
}

View file

@ -0,0 +1,9 @@
import { useState } from 'react';
//create your forceUpdate hook
export function useForceUpdate() {
const [value, setValue] = useState(0); // integer state
return () => setValue(() => value + 1); // update state to force render
// A function that increment 👆🏻 the previous state like here
// is better than directly setting `setValue(value + 1)`
}

View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View file

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: './',
server: {
fs: {
allow: ['..']
}
}
})

File diff suppressed because it is too large Load diff

106
src/interpreter/mod.rs Normal file
View file

@ -0,0 +1,106 @@
use std::{
collections::HashMap,
io::{BufReader, Read},
};
use crate::{
quantity::{
units::{UnitCombo, UnitSystem},
Quantity, QuantityError,
},
tokenizer::{token::Token, ReaderCursor, Tokenizer, TokenizerError},
};
use thiserror::Error;
pub mod ops;
pub mod ops_macros;
pub mod ops_variables;
pub struct Interpreter<'a> {
variables: HashMap<String, Quantity>,
unit_system: UnitSystem,
stack: Vec<Quantity>,
output: Box<dyn Fn(Output) + 'a>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Output {
Quantity(Quantity),
QuantityList(Vec<Quantity>),
Message(String),
}
#[derive(Debug, Error)]
pub enum InterpreterError {
#[error("Tokenizer error: {1} at {0}")]
TokenizerError(ReaderCursor, TokenizerError),
#[error("Quantity error: {0}")]
QuantityError(QuantityError),
#[error("Stack underflow")]
StackUnderflow,
#[error("Undefined unit: {0}")]
UndefinedUnit(String),
#[error("Undefined macro: {0}")]
UndefinedMacro(String),
#[error("Undefined variable: {0}")]
UndefinedVariable(String),
#[error("Incompatible units: {0}")]
IncompatibleUnits(UnitCombo),
#[error("Already defined: {0}")]
AlreadyDefined(String),
}
pub type InterpreterResult<T> = Result<T, InterpreterError>;
impl<'a> Interpreter<'a> {
pub fn new(output: Box<dyn Fn(Output) + 'a>) -> Self {
Self {
variables: HashMap::new(),
unit_system: UnitSystem::new(),
stack: Vec::new(),
output,
}
}
pub fn process_tokens<R: Read>(
&mut self,
tokenizer: &mut Tokenizer<R>,
) -> InterpreterResult<()> {
while let Some(token) = tokenizer
.parse_next_token()
.map_err(|e| InterpreterError::TokenizerError(tokenizer.get_cursor(), e))?
{
match token {
Token::Number(n) => self.op_number(n)?,
Token::Unit(u) => self.op_unit(&u)?,
Token::Add => self.op_add()?,
Token::Sub => self.op_sub()?,
Token::Mul => self.op_mul()?,
Token::Div => self.op_div()?,
Token::Operator('p') => self.op_p()?,
Token::Operator('n') => self.op_n()?,
Token::Operator('f') => self.op_f()?,
Token::Operator('c') => self.op_c()?,
Token::Operator('d') => self.op_d()?,
Token::Operator('r') => self.op_r()?,
Token::VarRecall(name) => self.op_recall(&name)?,
Token::VarStore(name) => self.op_store(&name)?,
Token::MacroInvoke((name, args)) => match name.as_str() {
"base" => self.op_macro_baseunit(&args)?,
"derived" => self.op_macro_derivedunit(&args)?,
_ => return Err(InterpreterError::UndefinedMacro(name)),
},
Token::Comment(_) => {}
_ => eprintln!("Unhandled token: {:?}", token),
}
}
Ok(())
}
pub fn run_str(&mut self, input: &str) -> InterpreterResult<()> {
let mut tokenizer = Tokenizer::new(BufReader::new(input.as_bytes()));
self.process_tokens(&mut tokenizer)?;
Ok(())
}
}

143
src/interpreter/ops.rs Normal file
View file

@ -0,0 +1,143 @@
use num_rational::BigRational;
use crate::quantity::{
units::{Unit, UnitCombo},
Quantity,
};
use super::{Interpreter, InterpreterError, InterpreterResult, Output};
impl<'a> Interpreter<'a> {
pub fn op_number(&mut self, number: BigRational) -> InterpreterResult<()> {
self.stack.push(Quantity::new(number, UnitCombo::new()));
Ok(())
}
pub fn op_unit(&mut self, unit: &str) -> InterpreterResult<()> {
let mut q = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
// Unitless, remove unit
if unit == "1" {
q.unit = UnitCombo::new();
self.stack.push(q);
return Ok(());
}
let unit = self
.unit_system
.lookup_unit(unit)
.ok_or(InterpreterError::UndefinedUnit(unit.to_string()))?;
match unit {
Unit::Base(base_unit) => {
let mut new_unit = UnitCombo::new();
new_unit.push_base_unit(base_unit.clone(), 1);
if q.unit.is_unitless() {
q.unit = new_unit;
} else if q.unit == new_unit {
q.use_derived_unit.clear();
} else {
return Err(InterpreterError::IncompatibleUnits(q.unit));
}
}
Unit::Derived(derived_unit) => {
let mut new_unit = UnitCombo::new();
new_unit.push_derived_unit(derived_unit.clone());
if q.unit == new_unit {
q.use_derived_unit
.retain(|u| u.exponents != derived_unit.exponents);
q.use_derived_unit.push(derived_unit.clone());
} else if q.unit.is_unitless() {
q.number *= derived_unit.scale.clone();
q.number += derived_unit.offset.clone();
q.unit = new_unit;
q.use_derived_unit
.retain(|u| u.exponents != derived_unit.exponents);
q.use_derived_unit.push(derived_unit.clone());
} else {
return Err(InterpreterError::IncompatibleUnits(q.unit));
}
}
}
self.stack.push(q);
Ok(())
}
pub fn op_add(&mut self) -> InterpreterResult<()> {
let rhs = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
let lhs = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
self.stack
.push((lhs + rhs).map_err(InterpreterError::QuantityError)?);
Ok(())
}
pub fn op_sub(&mut self) -> InterpreterResult<()> {
let rhs = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
let lhs = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
self.stack
.push((lhs - rhs).map_err(InterpreterError::QuantityError)?);
Ok(())
}
pub fn op_mul(&mut self) -> InterpreterResult<()> {
let rhs = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
let lhs = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
self.stack.push(lhs * rhs);
Ok(())
}
pub fn op_div(&mut self) -> InterpreterResult<()> {
let rhs = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
let lhs = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
self.stack.push(lhs / rhs);
Ok(())
}
pub fn op_p(&mut self) -> InterpreterResult<()> {
let q = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
(self.output)(Output::Quantity(q.clone()));
self.stack.push(q);
Ok(())
}
pub fn op_n(&mut self) -> InterpreterResult<()> {
let q = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
(self.output)(Output::Quantity(q));
Ok(())
}
pub fn op_f(&mut self) -> InterpreterResult<()> {
(self.output)(Output::QuantityList(self.stack.clone()));
Ok(())
}
pub fn op_d(&mut self) -> InterpreterResult<()> {
let q = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
self.stack.push(q.clone());
self.stack.push(q);
Ok(())
}
pub fn op_c(&mut self) -> InterpreterResult<()> {
self.stack.clear();
Ok(())
}
pub fn op_r(&mut self) -> InterpreterResult<()> {
let a = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
let b = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
self.stack.push(a);
self.stack.push(b);
Ok(())
}
}

View file

@ -0,0 +1,38 @@
use crate::quantity::units::{BaseUnit, DerivedUnit};
use super::{Interpreter, InterpreterError, InterpreterResult};
impl<'a> Interpreter<'a> {
pub fn op_macro_baseunit(&mut self, arg: &str) -> InterpreterResult<()> {
let symbol = arg.trim();
if self.unit_system.lookup_unit(symbol).is_some() {
return Err(InterpreterError::AlreadyDefined(symbol.to_string()));
}
self.unit_system.push_base_unit(BaseUnit {
symbol: symbol.to_string(),
});
Ok(())
}
pub fn op_macro_derivedunit(&mut self, arg: &str) -> InterpreterResult<()> {
let symbol = arg.trim();
let scale = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
let offset = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
if self.unit_system.lookup_unit(symbol).is_some() {
return Err(InterpreterError::AlreadyDefined(symbol.to_string()));
}
self.unit_system.push_derived_unit(DerivedUnit {
symbol: symbol.to_string(),
scale: scale.number,
offset: offset.number,
exponents: offset.unit,
});
Ok(())
}
}

View file

@ -0,0 +1,26 @@
use super::{Interpreter, InterpreterError, InterpreterResult};
impl<'a> Interpreter<'a> {
pub fn op_store(&mut self, arg: &str) -> InterpreterResult<()> {
let symbol = arg.trim();
let q = self.stack.pop().ok_or(InterpreterError::StackUnderflow)?;
self.variables.insert(symbol.to_string(), q);
Ok(())
}
pub fn op_recall(&mut self, arg: &str) -> InterpreterResult<()> {
let symbol = arg.trim();
let q = self
.variables
.get(symbol)
.ok_or(InterpreterError::UndefinedVariable(symbol.to_string()))?
.clone();
self.stack.push(q);
Ok(())
}
}

3
src/lib.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod interpreter;
pub mod quantity;
pub mod tokenizer;

142
src/quantity/mod.rs Normal file
View file

@ -0,0 +1,142 @@
use std::{
fmt::Display,
ops::{Add, Div, Mul, Sub},
};
use num_rational::BigRational;
use num_traits::ToPrimitive;
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
use thiserror::Error;
pub mod units;
use units::{DerivedUnit, UnitCombo};
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Quantity {
pub number: BigRational,
pub unit: UnitCombo,
pub use_derived_unit: Vec<DerivedUnit>,
}
impl Serialize for Quantity {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("Quantity", 5)?;
s.serialize_field("_str", &self.to_string())?;
s.serialize_field("number_float", &self.number.to_f64())?;
s.serialize_field("number", &self.number)?;
s.serialize_field("unit", &self.unit)?;
s.serialize_field("use_derived_unit", &self.use_derived_unit)?;
s.end()
}
}
#[derive(Error, Debug)]
pub enum QuantityError {
#[error("Incompatible units")]
IncompatibleUnits,
#[error("Unknown unit")]
UnknownUnit,
}
impl Quantity {
pub fn new(number: BigRational, unit: UnitCombo) -> Self {
Quantity {
number,
unit,
use_derived_unit: Vec::new(),
}
}
}
impl Display for Quantity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut number = self.number.clone();
for d in &self.use_derived_unit {
if d.exponents == self.unit {
number -= d.offset.clone();
number /= d.scale.clone();
write!(f, "{} ({})", number.to_f64().unwrap_or(f64::NAN), d)?;
return Ok(());
}
}
write!(f, "{} ({})", number.to_f64().unwrap_or(f64::NAN), self.unit)?;
Ok(())
}
}
impl Add for Quantity {
type Output = Result<Self, QuantityError>;
fn add(self, rhs: Self) -> Self::Output {
if self.unit != rhs.unit {
return Err(QuantityError::IncompatibleUnits);
}
let number = self.number + rhs.number;
let unit = self.unit;
let use_derived_unit = rhs.use_derived_unit;
Ok(Quantity {
number,
unit,
use_derived_unit,
})
}
}
impl Sub for Quantity {
type Output = Result<Self, QuantityError>;
fn sub(self, rhs: Self) -> Self::Output {
if self.unit != rhs.unit {
return Err(QuantityError::IncompatibleUnits);
}
let number = self.number - rhs.number;
let unit = self.unit;
let use_derived_unit = rhs.use_derived_unit;
Ok(Quantity {
number,
unit,
use_derived_unit,
})
}
}
impl Mul for Quantity {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
let number = self.number * rhs.number;
let unit = self.unit * rhs.unit;
let use_derived_unit = rhs.use_derived_unit;
Quantity {
number,
unit,
use_derived_unit,
}
}
}
impl Div for Quantity {
type Output = Self;
fn div(self, rhs: Self) -> Self::Output {
let number = self.number / rhs.number;
let unit = self.unit / rhs.unit;
let use_derived_unit = rhs.use_derived_unit;
Quantity {
number,
unit,
use_derived_unit,
}
}
}

203
src/quantity/units.rs Normal file
View file

@ -0,0 +1,203 @@
use std::{
collections::HashMap,
fmt::{Debug, Display},
ops::{Div, Mul, Neg},
};
use num_rational::BigRational;
use serde::{Deserialize, Serialize};
#[derive(Clone)]
pub struct UnitSystem {
base_units: HashMap<String, BaseUnit>,
derived_units: HashMap<String, DerivedUnit>,
}
pub enum Unit<'a> {
Base(&'a BaseUnit),
Derived(&'a DerivedUnit),
}
impl Debug for UnitSystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut base_units: Vec<&BaseUnit> = self.base_units.values().collect();
base_units.sort_by(|a, b| a.symbol.cmp(&b.symbol));
let mut derived_units: Vec<&DerivedUnit> = self.derived_units.values().collect();
derived_units.sort_by(|a, b| a.symbol.cmp(&b.symbol));
f.debug_struct("UnitSystem")
.field("base_units", &base_units)
.field("derived_units", &derived_units)
.finish()
}
}
impl UnitSystem {
pub fn new() -> Self {
UnitSystem {
base_units: HashMap::new(),
derived_units: HashMap::new(),
}
}
pub fn lookup_unit(&self, symbol: &str) -> Option<Unit> {
self.lookup_base_unit(symbol)
.map(Unit::Base)
.or_else(|| self.lookup_derived_unit(symbol).map(Unit::Derived))
}
pub fn lookup_base_unit(&self, symbol: &str) -> Option<&BaseUnit> {
self.base_units.get(symbol)
}
pub fn lookup_derived_unit(&self, symbol: &str) -> Option<&DerivedUnit> {
self.derived_units.get(symbol)
}
pub fn push_base_unit(&mut self, unit: BaseUnit) {
self.base_units.insert(unit.symbol.clone(), unit);
}
pub fn push_derived_unit(&mut self, unit: DerivedUnit) {
self.derived_units.insert(unit.symbol.clone(), unit);
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct BaseUnit {
pub symbol: String,
}
impl Display for BaseUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.symbol)
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct DerivedUnit {
pub symbol: String,
pub offset: BigRational,
pub scale: BigRational,
pub exponents: UnitCombo,
}
impl Display for DerivedUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.symbol)
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct UnitExponent {
pub unit: BaseUnit,
pub exponent: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnitCombo(pub Vec<UnitExponent>);
impl UnitCombo {
pub fn new() -> Self {
UnitCombo(Vec::new())
}
pub fn push_base_unit(&mut self, unit: BaseUnit, exponent: i32) {
for exponents in self.0.iter_mut() {
if exponents.unit == unit {
exponents.exponent += exponent;
if exponents.exponent == 0 {
self.0.retain(|e| e.unit != unit);
}
return;
}
}
self.0.push(UnitExponent { unit, exponent });
}
pub fn push_derived_unit(&mut self, unit: DerivedUnit) {
for c in unit.exponents.0 {
self.push_base_unit(c.unit, c.exponent);
}
}
}
impl Display for UnitCombo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut exponents: Vec<&UnitExponent> = self.0.iter().filter(|e| e.exponent != 0).collect();
if exponents.is_empty() {
write!(f, "1")?;
return Ok(());
}
exponents.sort_by(|a, b| b.exponent.cmp(&a.exponent));
for exponent in exponents.iter() {
if exponent.exponent == 1 {
write!(f, "{}", exponent.unit.symbol)?;
} else {
write!(f, "({}^{})", exponent.unit.symbol, exponent.exponent)?;
}
}
Ok(())
}
}
impl<'a> PartialEq for UnitCombo {
fn eq(&self, other: &Self) -> bool {
let mut self_exponents = self.0.clone();
let mut other_exponents = other.0.clone();
self_exponents.sort_by(|a, b| a.unit.symbol.cmp(&b.unit.symbol));
other_exponents.sort_by(|a, b| a.unit.symbol.cmp(&b.unit.symbol));
self_exponents == other_exponents
}
}
impl UnitCombo {
pub fn is_unitless(&self) -> bool {
self.0.is_empty()
}
pub fn reduce(&self) -> Self {
let mut exponents: Vec<UnitExponent> = Vec::new();
for component in self.0.iter() {
let mut found = false;
for exponent in exponents.iter_mut() {
if exponent.unit == component.unit {
exponent.exponent += component.exponent;
found = true;
break;
}
}
if !found {
exponents.push(component.clone());
}
}
UnitCombo(
exponents
.iter()
.filter(|e| e.exponent != 0)
.cloned()
.collect(),
)
}
}
impl Mul for UnitCombo {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
let mut new_exponents = self.0.clone();
new_exponents.extend(rhs.0);
UnitCombo(new_exponents).reduce()
}
}
impl Neg for UnitCombo {
type Output = Self;
fn neg(self) -> Self::Output {
let mut new_exponents = self.0.clone();
for exponent in new_exponents.iter_mut() {
exponent.exponent *= -1;
}
UnitCombo(new_exponents).reduce()
}
}
impl Div for UnitCombo {
type Output = Self;
fn div(self, rhs: Self) -> Self::Output {
self * -rhs
}
}

258
src/tokenizer/mod.rs Normal file
View file

@ -0,0 +1,258 @@
use std::{
fmt::Display,
io::{BufReader, Read},
};
pub mod parsing;
pub mod token;
use thiserror::Error;
use token::Token;
use self::parsing::parse_bigrational;
pub struct Tokenizer<R>
where
R: std::io::Read,
{
input: BufReader<R>,
unread_buffer: Option<char>,
cursor: ReaderCursor,
}
#[derive(Error, Debug)]
pub enum TokenizerError {
#[error("IO Error: {0}")]
IOError(std::io::Error),
#[error("Invalid character: {0}")]
InvalidCharacter(char),
}
#[derive(Debug, Clone, Copy)]
pub struct ReaderCursor {
pub line: usize,
pub column: usize,
}
impl Display for ReaderCursor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
impl ReaderCursor {
pub fn new() -> Self {
ReaderCursor { line: 1, column: 1 }
}
}
impl<R: std::io::Read> Tokenizer<R> {
pub fn new(input: R) -> Self {
Tokenizer {
input: BufReader::new(input),
unread_buffer: None,
cursor: ReaderCursor::new(),
}
}
fn next_char(&mut self) -> Result<Option<char>, std::io::Error> {
if let Some(ch) = self.unread_buffer {
self.unread_buffer = None;
return Ok(Some(ch));
}
let mut buf = [0; 1];
match self.input.read(&mut buf) {
Ok(0) => Ok(None),
Ok(_) => {
if buf[0] == '\n' as u8 {
self.cursor.line += 1;
self.cursor.column = 1;
} else {
self.cursor.column += 1;
}
Ok(Some(buf[0] as char))
}
Err(e) => Err(e),
}
}
fn next_char_non_whitespace(&mut self) -> Result<Option<char>, std::io::Error> {
loop {
match self.next_char()? {
Some(ch) if ch.is_whitespace() => {}
ch => return Ok(ch),
}
}
}
fn unread_char(&mut self, ch: char) {
if self.unread_buffer.is_some() {
panic!("Cannot unread more than one character");
}
self.unread_buffer = Some(ch);
}
pub fn get_cursor(&self) -> ReaderCursor {
self.cursor
}
pub fn parse_next_token(&mut self) -> Result<Option<Token>, TokenizerError> {
let mut buf = String::new();
let ch = self
.next_char_non_whitespace()
.map_err(TokenizerError::IOError)?;
match ch {
Some('0'..='9' | '_') => {
buf.push(ch.unwrap());
while let Some(c) = self.next_char().map_err(TokenizerError::IOError)? {
match c {
'0'..='9' => buf.push(c),
'.' | 'e' | 'E' | '_' | '-' => buf.push(c),
_ => {
self.unread_char(c);
break;
}
}
}
Ok(Some(Token::Number(parse_bigrational(&buf)?)))
}
Some('(') => {
while let Some(c) = self.next_char().map_err(TokenizerError::IOError)? {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '/' | '*' | '_' => buf.push(c),
')' => break,
_ => return Err(TokenizerError::InvalidCharacter(c)),
}
}
Ok(Some(Token::Unit(buf)))
}
Some('@') => {
while let Some(c) = self.next_char().map_err(TokenizerError::IOError)? {
match c {
'a'..='z' | 'A'..='Z' | '_' => buf.push(c),
'(' => {
break;
}
_ => return Err(TokenizerError::InvalidCharacter(c)),
}
}
let macro_name = buf.clone();
buf.clear();
while let Some(c) = self.next_char().map_err(TokenizerError::IOError)? {
match c {
')' => break,
_ => buf.push(c),
}
}
Ok(Some(Token::MacroInvoke((macro_name, buf))))
}
Some('+') => Ok(Some(Token::Add)),
Some('-') => Ok(Some(Token::Sub)),
Some('*') => Ok(Some(Token::Mul)),
Some('/') => Ok(Some(Token::Div)),
Some('p') => Ok(Some(Token::Operator('p'))),
Some('n') => Ok(Some(Token::Operator('n'))),
Some('f') => Ok(Some(Token::Operator('f'))),
Some('c') => Ok(Some(Token::Operator('c'))),
Some('d') => Ok(Some(Token::Operator('d'))),
Some('r') => Ok(Some(Token::Operator('r'))),
Some('#') => {
while let Some(c) = self.next_char().map_err(TokenizerError::IOError)? {
match c {
'\r' | '\n' => break,
_ => buf.push(c),
}
}
Ok(Some(Token::Comment(buf)))
}
Some('>') => {
while let Some(c) = self.next_char().map_err(TokenizerError::IOError)? {
match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '_' => buf.push(c),
_ => {
self.unread_char(c);
break;
}
}
}
Ok(Some(Token::VarStore(buf)))
}
Some('<') => {
while let Some(c) = self.next_char().map_err(TokenizerError::IOError)? {
match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '_' => buf.push(c),
_ => {
self.unread_char(c);
break;
}
}
}
Ok(Some(Token::VarRecall(buf)))
}
None => Ok(None),
_ => Err(TokenizerError::InvalidCharacter(ch.unwrap())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use num_rational::BigRational;
use num_traits::FromPrimitive;
#[test]
fn test_tokenizer() {
let input = "1 2e3+ 3.4e5* (g) 4.5(ml) / 6_789 + 3.14".as_bytes();
let mut tokenizer = Tokenizer::new(input);
assert!(tokenizer
.parse_next_token()
.unwrap()
.unwrap()
.roughly_eq(&Token::Number(
BigRational::from_i64(1).expect("Failed to parse number")
)));
assert!(tokenizer
.parse_next_token()
.unwrap()
.unwrap()
.roughly_eq(&Token::Number(
BigRational::from_i64(2e3 as i64).expect("Failed to parse number")
)));
assert_eq!(tokenizer.parse_next_token().unwrap().unwrap(), Token::Add);
assert!(tokenizer
.parse_next_token()
.unwrap()
.unwrap()
.roughly_eq(&Token::Number(
BigRational::from_f64(3.4e5).expect("Failed to parse number")
)));
assert_eq!(tokenizer.parse_next_token().unwrap().unwrap(), Token::Mul);
assert_eq!(
tokenizer.parse_next_token().unwrap().unwrap(),
Token::Unit("g".to_string())
);
assert!(tokenizer
.parse_next_token()
.unwrap()
.unwrap()
.roughly_eq(&Token::Number(
BigRational::from_f64(4.5).expect("Failed to parse number")
)));
assert_eq!(
tokenizer.parse_next_token().unwrap().unwrap(),
Token::Unit("ml".to_string())
);
assert_eq!(tokenizer.parse_next_token().unwrap().unwrap(), Token::Div);
assert!(tokenizer
.parse_next_token()
.unwrap()
.unwrap()
.roughly_eq(&Token::Number(
BigRational::from_i64(6789).expect("Failed to parse number")
)));
assert_eq!(tokenizer.parse_next_token().unwrap().unwrap(), Token::Add);
assert!(tokenizer
.parse_next_token()
.unwrap()
.unwrap()
.roughly_eq(&Token::Number(
BigRational::from_f64(3.14).expect("Failed to parse number")
)));
}
}

92
src/tokenizer/parsing.rs Normal file
View file

@ -0,0 +1,92 @@
use super::TokenizerError;
use num_bigint::{BigInt, BigUint};
use num_rational::BigRational;
use num_traits::pow::Pow;
use std::collections::VecDeque;
pub fn parse_bigrational(s: &str) -> Result<BigRational, TokenizerError> {
let mut numerator = BigInt::from(0);
let mut is_decimal = false;
let mut decimal_places = 0;
let mut is_negative = false;
let mut is_exponent = false;
let mut exponent = BigInt::from(0);
let mut exponent_is_negative = false;
let mut s = s.chars().filter(|c| *c != '_').collect::<VecDeque<_>>();
if s[0] == '-' {
is_negative = true;
s.pop_front();
}
while let Some(ch) = s.pop_front() {
match ch {
'0'..='9' => {
if is_exponent {
let digit = ch.to_digit(10).unwrap();
let exponent_digit = BigInt::from(digit);
exponent *= BigInt::from(10);
exponent += exponent_digit;
} else if is_decimal {
let digit = ch.to_digit(10).unwrap();
let numerator_digit = BigInt::from(digit);
numerator *= BigInt::from(10);
numerator += numerator_digit;
decimal_places += 1;
} else {
let digit = ch.to_digit(10).unwrap();
let numerator_digit = BigInt::from(digit);
numerator *= BigInt::from(10);
numerator += numerator_digit;
}
}
'.' => {
if is_decimal {
return Err(TokenizerError::InvalidCharacter(ch));
}
is_decimal = true;
}
'e' | 'E' => {
if is_exponent {
return Err(TokenizerError::InvalidCharacter(ch));
}
is_exponent = true;
if let Some(ch) = s.pop_front() {
if ch == '-' {
exponent_is_negative = true;
} else {
s.push_front(ch);
}
}
}
_ => return Err(TokenizerError::InvalidCharacter(ch))?,
}
}
if is_negative {
numerator = -numerator;
}
if exponent_is_negative {
exponent = -exponent;
}
let without_exponent = BigRational::new_raw(numerator, BigInt::from(1));
let exponent = exponent - BigInt::from(decimal_places);
let exp = if exponent >= BigInt::from(0) {
BigRational::new_raw(
BigInt::from(10).pow(BigUint::try_from(exponent).unwrap()),
BigInt::from(1),
)
} else {
BigRational::new_raw(
BigInt::from(1),
BigInt::from(10).pow(BigUint::try_from(-exponent).unwrap()),
)
};
Ok((without_exponent * exp).reduced())
}

32
src/tokenizer/token.rs Normal file
View file

@ -0,0 +1,32 @@
use num_rational::BigRational;
use num_traits::ToPrimitive;
#[derive(Debug, PartialEq)]
pub enum Token {
Number(BigRational),
Unit(String),
Add,
Sub,
Mul,
Div,
VarStore(String),
VarRecall(String),
Operator(char),
MacroInvoke((String, String)),
Comment(String),
}
impl Token {
pub fn roughly_eq(&self, other: &Self) -> bool {
match (self, other) {
(Token::Number(a), Token::Number(b)) => a.to_f64().unwrap() == b.to_f64().unwrap(),
(Token::Unit(a), Token::Unit(b)) => a == b,
(Token::Add, Token::Add) => true,
(Token::Sub, Token::Sub) => true,
(Token::Mul, Token::Mul) => true,
(Token::Div, Token::Div) => true,
(Token::Operator(a), Token::Operator(b)) => a == b,
_ => false,
}
}
}

60
tests/interpreter_test.rs Normal file
View file

@ -0,0 +1,60 @@
use std::sync::Mutex;
use num_traits::ToPrimitive;
use unitdc::{
interpreter::Interpreter,
quantity::units::{BaseUnit, UnitCombo, UnitExponent},
};
#[test]
fn test_interpreter() {
let outputs = Mutex::new(Vec::new());
let output_fn = |output| outputs.lock().unwrap().push(output);
let mut interpreter = Interpreter::new(Box::new(output_fn));
interpreter
.run_str("@base(m)")
.expect("command should succeed");
interpreter
.run_str("0 (m) 1e3 @derived(km)")
.expect("command should succeed");
interpreter
.run_str("2 (km) p")
.expect("command should succeed");
let output = outputs.lock().unwrap().pop().expect("output should exist");
match output {
unitdc::interpreter::Output::Quantity(q) => {
assert_eq!(q.number.to_f64().unwrap(), 2000.0);
assert_eq!(
q.unit,
UnitCombo(vec![UnitExponent {
unit: BaseUnit {
symbol: "m".to_string(),
},
exponent: 1,
},])
);
}
_ => panic!("output should be a quantity"),
}
interpreter
.run_str("2 (km) 1 (m) + p")
.expect("command should succeed");
let output = outputs.lock().unwrap().pop().expect("output should exist");
match output {
unitdc::interpreter::Output::Quantity(q) => {
assert_eq!(q.number.to_f64().unwrap(), 2001.0);
assert_eq!(
q.unit,
UnitCombo(vec![UnitExponent {
unit: BaseUnit {
symbol: "m".to_string(),
},
exponent: 1,
},])
);
}
_ => panic!("output should be a quantity"),
}
}

40
unitdc.rc Normal file
View file

@ -0,0 +1,40 @@
@base(iu)
@base(m)
0 (m) 1e3 @derived(km)
0 (m) 1e-2 @derived(cm)
0 (m) 1e-3 @derived(mm)
0 (m) 1e-6 @derived(um)
0 (m) 1e-9 @derived(nm)
0 (m) 1e-12 @derived(pm)
0 (m) 1 3.280839895 / @derived(ft)
@base(degC)
0 (degC) 273.15 @derived(K)
_-5 9 / 32 * (degC) 5 9 / @derived(degF)
@base(mol)
@base(l)
0 (l) 1e-1 @derived(dl)
0 (l) 1e-3 @derived(ml)
0 (l) 1e-6 @derived(ul)
0 (l) 1e-9 @derived(nl)
0 (mol) 1 (l) / 1 @derived(M)
0 (mol) 1 (l) / 1e-3 @derived(mM)
0 (mol) 1 (l) / 1e-6 @derived(uM)
0 (mol) 1 (l) / 1e-9 @derived(nM)
@base(g)
0 (g) 1e3 @derived(kg)
0 (g) 1e-3 @derived(mg)
0 (g) 1e-6 @derived(ug)
0 (g) 1e-9 @derived(ng)
0 (g) 1e-12 @derived(pg)
0 (g) 100 (ml) / 1e-2 @derived(w/v)
0 (g) 1 (mol) / 1 @derived(Da)
0 (g) 1 (mol) / 1e3 @derived(kDa)