mirror of
https://github.com/eternal-flame-AD/unitdc-rs.git
synced 2024-11-24 03:26:43 -06:00
init
This commit is contained in:
commit
d3d2327112
39 changed files with 3612 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/Cargo.lock
|
24
Cargo.toml
Normal file
24
Cargo.toml
Normal 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"
|
9
crates/unitdc-cli/Cargo.toml
Normal file
9
crates/unitdc-cli/Cargo.toml
Normal 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 = "../../" }
|
25
crates/unitdc-cli/src/main.rs
Normal file
25
crates/unitdc-cli/src/main.rs
Normal 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
6
crates/unitdc-web/.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
bin/
|
||||
pkg/
|
||||
wasm-pack.log
|
34
crates/unitdc-web/Cargo.toml
Normal file
34
crates/unitdc-web/Cargo.toml
Normal 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"
|
75
crates/unitdc-web/src/lib.rs
Normal file
75
crates/unitdc-web/src/lib.rs
Normal 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(())
|
||||
}
|
10
crates/unitdc-web/src/utils.rs
Normal file
10
crates/unitdc-web/src/utils.rs
Normal 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();
|
||||
}
|
14
crates/unitdc-web/ui/.eslintrc.cjs
Normal file
14
crates/unitdc-web/ui/.eslintrc.cjs
Normal 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
24
crates/unitdc-web/ui/.gitignore
vendored
Normal 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?
|
13
crates/unitdc-web/ui/index.html
Normal file
13
crates/unitdc-web/ui/index.html
Normal 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>
|
28
crates/unitdc-web/ui/package.json
Normal file
28
crates/unitdc-web/ui/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
crates/unitdc-web/ui/public/vite.svg
Normal file
1
crates/unitdc-web/ui/public/vite.svg
Normal 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 |
123
crates/unitdc-web/ui/src/App.css
Normal file
123
crates/unitdc-web/ui/src/App.css
Normal 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;
|
||||
}
|
225
crates/unitdc-web/ui/src/App.tsx
Normal file
225
crates/unitdc-web/ui/src/App.tsx
Normal 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
|
12
crates/unitdc-web/ui/src/components/ErrorCell.tsx
Normal file
12
crates/unitdc-web/ui/src/components/ErrorCell.tsx
Normal 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>
|
||||
)
|
||||
}
|
58
crates/unitdc-web/ui/src/components/InputCell.tsx
Normal file
58
crates/unitdc-web/ui/src/components/InputCell.tsx
Normal 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>
|
||||
)
|
||||
}
|
140
crates/unitdc-web/ui/src/components/Keyboard.tsx
Normal file
140
crates/unitdc-web/ui/src/components/Keyboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
16
crates/unitdc-web/ui/src/components/OutputCell.tsx
Normal file
16
crates/unitdc-web/ui/src/components/OutputCell.tsx
Normal 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>
|
||||
)
|
||||
}
|
9
crates/unitdc-web/ui/src/main.tsx
Normal file
9
crates/unitdc-web/ui/src/main.tsx
Normal 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>,
|
||||
)
|
16
crates/unitdc-web/ui/src/types.ts
Normal file
16
crates/unitdc-web/ui/src/types.ts
Normal 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;
|
||||
}
|
9
crates/unitdc-web/ui/src/util.ts
Normal file
9
crates/unitdc-web/ui/src/util.ts
Normal 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)`
|
||||
}
|
1
crates/unitdc-web/ui/src/vite-env.d.ts
vendored
Normal file
1
crates/unitdc-web/ui/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
25
crates/unitdc-web/ui/tsconfig.json
Normal file
25
crates/unitdc-web/ui/tsconfig.json
Normal 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" }]
|
||||
}
|
10
crates/unitdc-web/ui/tsconfig.node.json
Normal file
10
crates/unitdc-web/ui/tsconfig.node.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
13
crates/unitdc-web/ui/vite.config.ts
Normal file
13
crates/unitdc-web/ui/vite.config.ts
Normal 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: ['..']
|
||||
}
|
||||
}
|
||||
})
|
1547
crates/unitdc-web/ui/yarn.lock
Normal file
1547
crates/unitdc-web/ui/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
106
src/interpreter/mod.rs
Normal file
106
src/interpreter/mod.rs
Normal 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
143
src/interpreter/ops.rs
Normal 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(())
|
||||
}
|
||||
}
|
38
src/interpreter/ops_macros.rs
Normal file
38
src/interpreter/ops_macros.rs
Normal 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(())
|
||||
}
|
||||
}
|
26
src/interpreter/ops_variables.rs
Normal file
26
src/interpreter/ops_variables.rs
Normal 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
3
src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod interpreter;
|
||||
pub mod quantity;
|
||||
pub mod tokenizer;
|
142
src/quantity/mod.rs
Normal file
142
src/quantity/mod.rs
Normal 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
203
src/quantity/units.rs
Normal 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
258
src/tokenizer/mod.rs
Normal 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
92
src/tokenizer/parsing.rs
Normal 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
32
src/tokenizer/token.rs
Normal 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
60
tests/interpreter_test.rs
Normal 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
40
unitdc.rc
Normal 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)
|
Loading…
Reference in a new issue