Compare commits

...

10 Commits

Author SHA1 Message Date
2cca20ae00 add currency compound units by division and multiplication
Some checks are pending
Deploy / web (push) Waiting to run
Test / test (push) Waiting to run
2026-03-03 00:14:40 -05:00
7e76fc4008 add currency 2025-12-11 20:11:53 -05:00
Kasper
49b05d0876
Add autofocus 2025-06-23 07:24:44 +02:00
Kasper
9c56b4ddb5
Fix website output 2025-06-17 08:41:41 +02:00
Kasper
9b49d8c418
Remove pow accuracy workaround
https://github.com/neogenie/fastnum/issues/28
2025-06-03 01:54:05 +02:00
Kasper
f918f3d5e0
Fix prerendering 2025-06-02 03:07:28 +02:00
Kasper
51774f9b38
Update preview.yml 2025-06-02 01:50:07 +02:00
Kasper
83c7d78fb2
Update deps 2025-05-30 16:47:57 +02:00
Kasper
eccfad5321
Add page title 2025-05-30 11:54:21 +02:00
Kasper
6a64d20b7e
Fix ci 2025-05-30 11:50:28 +02:00
16 changed files with 1586 additions and 100 deletions

View File

@ -2,7 +2,7 @@ name: Deploy
on:
push:
branches:
- master
- main
workflow_dispatch:
env:

View File

@ -1,8 +1,8 @@
name: Deploy
name: Preview
on:
push:
branches-ignore:
- master
- main
workflow_dispatch:
env:

610
Cargo.lock generated
View File

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -17,6 +23,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bnum"
version = "0.12.1"
@ -29,6 +41,16 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cc"
version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -52,22 +74,190 @@ dependencies = [
"console_error_panic_hook",
"fastnum",
"js-sys",
"once_cell",
"regex",
"serde",
"serde_json",
"unicode-segmentation",
"ureq",
"wasm-bindgen",
"web-time",
]
[[package]]
name = "fastnum"
version = "0.2.8"
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86cde0c9334bfed5ced962bd7acc266e02e254d71494787e4255d8ec4f7296d4"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "fastnum"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b875e26379edd7866a74cec720c6ae7bea3107aaf9fd3b14d7449d87d4c1986b"
dependencies = [
"autocfg",
"bnum",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "flate2"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "icu_collections"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
dependencies = [
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
[[package]]
name = "icu_properties"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
dependencies = [
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
[[package]]
name = "icu_provider"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.77"
@ -78,6 +268,18 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "litemap"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "log"
version = "0.4.27"
@ -90,12 +292,37 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "potential_utf"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
dependencies = [
"zerovec",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@ -143,12 +370,140 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"untrusted",
"windows-sys",
]
[[package]]
name = "rustls"
version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.101"
@ -160,6 +515,27 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
@ -172,6 +548,54 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
dependencies = [
"base64",
"flate2",
"log",
"once_cell",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"url",
"webpki-roots 0.26.11",
]
[[package]]
name = "url"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
@ -239,3 +663,183 @@ dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.4",
]
[[package]]
name = "webpki-roots"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "writeable"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "yoke"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
dependencies = [
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -10,6 +10,7 @@ homepage = "https://github.com/probablykasper/cpc#readme"
repository = "https://github.com/probablykasper/cpc"
documentation = "https://docs.rs/cpc"
keywords = ["math", "expression", "evaluate", "units", "convert"]
autobins = false
categories = [
"mathematics",
"science",
@ -18,6 +19,10 @@ categories = [
"value-formatting",
]
[[bin]]
name = "cpc_cli"
path = "src/main.rs"
[lib]
crate-type = ["cdylib", "rlib"]
@ -25,6 +30,12 @@ crate-type = ["cdylib", "rlib"]
fastnum = "0.2"
unicode-segmentation = "1.12"
web-time = "1.1.0"
once_cell = "1.20"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
ureq = { version = "2.12", features = ["json"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"

145
src/currency.rs Normal file
View File

@ -0,0 +1,145 @@
use crate::extra_units;
use crate::units::UnitType;
use fastnum::decimal::Context;
use fastnum::D128;
use serde_json::Value;
const EXCHANGE_RATE_API_USD_LATEST: &str = "https://api.exchangerate-api.com/v4/latest/USD";
fn parse_d128_from_json_number(v: &Value) -> Result<D128, String> {
let s = match v {
Value::Number(n) => n.to_string(),
_ => return Err("Expected JSON number".to_string()),
};
D128::from_str(&s, Context::default())
.map_err(|_| format!("Invalid decimal number: {s}"))
}
/// Registers/updates currencies from the ExchangeRate API JSON response (base USD).
///
/// The JSON is expected to look like `{"base":"USD","rates":{"EUR":0.852,...}}`.
///
/// Internally we store a "weight" as USD-per-unit, so conversions use the existing
/// `from.weight()/to.weight()` logic.
pub fn register_currencies_from_exchange_rate_api_json(json: &str) -> Result<usize, String> {
let v: Value = serde_json::from_str(json).map_err(|e| format!("Invalid JSON: {e}"))?;
let base = v
.get("base")
.and_then(|b| b.as_str())
.ok_or("Missing 'base'")?;
if base != "USD" {
return Err(format!("Expected base USD but got {base}"));
}
let rates = v
.get("rates")
.and_then(|r| r.as_object())
.ok_or("Missing 'rates' object")?;
let mut count = 0usize;
for (code, rate_value) in rates {
// Skip weird/empty keys defensively
if code.len() != 3 {
continue;
}
let code_upper = code.to_ascii_uppercase();
let code_lower = code.to_ascii_lowercase();
let rate = parse_d128_from_json_number(rate_value)?;
if rate.is_zero() {
continue;
}
// weight = USD per 1 {CODE} = 1 / (CODE per 1 USD)
let weight = D128::ONE / rate;
extra_units::upsert_unit(
&code_lower,
&code_upper,
&code_upper,
UnitType::Currency,
weight,
);
count += 1;
}
Ok(count)
}
#[cfg(not(target_arch = "wasm32"))]
fn cache_path() -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push("cpc_currency_rates_usd.json");
p
}
#[cfg(not(target_arch = "wasm32"))]
pub fn init_currencies_blocking() -> Result<usize, String> {
// 1) Load cached (if any) so currencies work offline.
let mut cached_count = 0usize;
let cache_path = cache_path();
if let Ok(s) = std::fs::read_to_string(&cache_path) {
if let Ok(n) = register_currencies_from_exchange_rate_api_json(&s) {
cached_count = n;
}
}
// 2) Try refresh from network.
let resp = ureq::get(EXCHANGE_RATE_API_USD_LATEST)
.call()
.map_err(|e| format!("Currency fetch failed: {e}"))?;
let body = resp
.into_string()
.map_err(|e| format!("Currency fetch read failed: {e}"))?;
let n = register_currencies_from_exchange_rate_api_json(&body)?;
// Best-effort cache write
let _ = std::fs::write(&cache_path, body);
Ok(std::cmp::max(cached_count, n))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extra_units;
use crate::units::{convert, Unit};
use crate::Number;
use fastnum::dec128 as d;
#[test]
fn register_and_convert_currency() {
extra_units::clear_for_tests();
let json = r#"{
"base": "USD",
"rates": {
"USD": 1,
"EUR": 0.5
}
}"#;
let n = register_currencies_from_exchange_rate_api_json(json).unwrap();
assert_eq!(n, 2);
let usd = extra_units::lookup_unit("usd").unwrap();
let eur = extra_units::lookup_unit("eur").unwrap();
let one_usd = Number::new(d!(1), usd);
let eur_amount = convert(one_usd, eur).unwrap();
assert_eq!(eur_amount.value, d!(0.5));
let one_eur = Number::new(d!(1), eur);
let usd_amount = convert(one_eur, usd).unwrap();
assert_eq!(usd_amount.value, d!(2));
// Display name should be currency code
assert_eq!(eur_amount.unit.singular(), "EUR");
assert_eq!(eur_amount.unit.plural(), "EUR");
// Sanity: it's still a Unit
let _ = Unit::NoUnit;
}
}

View File

@ -279,6 +279,9 @@ fn evaluate_node(ast_node: &AstNode) -> Result<Number, String> {
mod tests {
use crate::eval;
use super::*;
use crate::currency;
use crate::extra_units;
use crate::units::UnitType;
fn eval_default<'a>(input: &'a str) -> Number {
let result = eval(input, true, false).unwrap();
@ -299,6 +302,8 @@ mod tests {
assert_eq!(eval_default("-1km to m"), Number::new(d!(-1000), Unit::Meter));
assert_eq!(eval_num("2*-3*0.5"), "-3");
assert_eq!(eval_num("-3^2"), "-9");
assert_eq!(eval_num("e^2"), "7.3890560989306502272304274605750078132");
assert_eq!(eval_num("e^2.5"), "12.1824939607034734380701759511679661832");
assert_eq!(eval_num("-1+2"), "1");
}
@ -334,4 +339,57 @@ mod tests {
assert_eq!(eval_num("sin(2)"), "0.9092974268256816953960198659117448427");
assert_eq!(eval_num("sin(-2)"), "-0.9092974268256816953960198659117448427");
}
#[test]
fn test_divide_currency_and_mass_by_volume() {
// Ensure a clean registry so the IDs are deterministic
extra_units::clear_for_tests();
// Minimal rates (USD base). 1 USD = 7.07 CNY
let json = r#"{"base":"USD","rates":{"USD":1,"CNY":7.07}}"#;
currency::register_currencies_from_exchange_rate_api_json(json).unwrap();
let x = eval_default("5.55 usd / 64 fl oz");
assert_eq!(x.unit.category(), UnitType::CurrencyPerVolume);
assert_eq!(x.unit.singular(), "USD/floz");
let y = eval_default("5.55 g / 64 fl oz");
assert_eq!(y.unit.category(), UnitType::MassPerVolume);
assert_eq!(y.unit.singular(), "g/floz");
// Conversion should work on the derived units:
let z = eval_default("5.55 usd / 64 fl oz to cny/l");
assert_eq!(z.unit.singular(), "CNY/l");
let w = eval_default("5.55 g / 64 fl oz to g/ml");
assert_eq!(w.unit.singular(), "g/ml");
}
#[test]
fn test_multiply_cancels_derived_units() {
extra_units::clear_for_tests();
let json = r#"{"base":"USD","rates":{"USD":1,"CNY":7.07}}"#;
currency::register_currencies_from_exchange_rate_api_json(json).unwrap();
// Your example: (USD/L) * floz -> USD
let out = eval_default("(1.48-1/3) usd / 1.25 l * 144 fl oz");
assert_eq!(out.unit.category(), UnitType::Currency);
assert_eq!(out.unit.singular(), "USD");
assert!(!out.value.is_nan());
let assert_close = |actual: D128, expected: D128| {
// Derived unit weights use division, so allow tiny rounding differences.
assert!((actual - expected).abs() < d!(1e-24));
};
// Cancellation sanity checks
let usd_back = eval_default("5.55 usd / 64 fl oz * 64 fl oz");
assert_eq!(usd_back.unit.singular(), "USD");
assert_close(usd_back.value, d!(5.55));
let g_back = eval_default("5.55 g / 64 fl oz * 64 fl oz");
assert_eq!(g_back.unit.singular(), "gram");
assert_close(g_back.value, d!(5.55));
}
}

125
src/extra_units.rs Normal file
View File

@ -0,0 +1,125 @@
use crate::units::{Unit, UnitType};
use fastnum::D128;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::RwLock;
#[derive(Clone, Copy)]
pub(crate) struct ExtraUnitDef {
pub(crate) category: UnitType,
pub(crate) weight: D128,
pub(crate) singular: &'static str,
pub(crate) plural: &'static str,
}
#[derive(Default)]
struct Registry {
by_name: HashMap<&'static str, u16>,
units: Vec<ExtraUnitDef>,
}
static REGISTRY: Lazy<RwLock<Registry>> = Lazy::new(|| RwLock::new(Registry::default()));
fn leak_str(s: String) -> &'static str {
Box::leak(s.into_boxed_str())
}
pub(crate) fn lookup_unit(word: &str) -> Option<Unit> {
let reg = REGISTRY.read().ok()?;
let id = *reg.by_name.get(word)?;
Some(Unit::Extra(id))
}
pub(crate) fn upsert_unit(
name_lowercase: &str,
singular: &str,
plural: &str,
category: UnitType,
weight: D128,
) -> Unit {
let mut reg = REGISTRY
.write()
.expect("extra unit registry lock poisoned");
if let Some(existing_id) = reg.by_name.get(name_lowercase).copied() {
let idx = existing_id as usize;
reg.units[idx] = ExtraUnitDef {
category,
weight,
singular: reg.units[idx].singular,
plural: reg.units[idx].plural,
};
return Unit::Extra(existing_id);
}
let id: u16 = reg
.units
.len()
.try_into()
.expect("too many extra units registered");
let key = leak_str(name_lowercase.to_string());
let singular = leak_str(singular.to_string());
let plural = leak_str(plural.to_string());
reg.by_name.insert(key, id);
reg.units.push(ExtraUnitDef {
category,
weight,
singular,
plural,
});
Unit::Extra(id)
}
pub(crate) fn get_category(id: u16) -> UnitType {
let reg = REGISTRY
.read()
.expect("extra unit registry lock poisoned");
reg.units
.get(id as usize)
.map(|u| u.category)
.unwrap_or(UnitType::NoType)
}
pub(crate) fn get_weight(id: u16) -> D128 {
let reg = REGISTRY
.read()
.expect("extra unit registry lock poisoned");
reg.units
.get(id as usize)
.map(|u| u.weight)
.unwrap_or(D128::NAN)
}
pub(crate) fn get_singular(id: u16) -> &'static str {
let reg = REGISTRY
.read()
.expect("extra unit registry lock poisoned");
reg.units
.get(id as usize)
.map(|u| u.singular)
.unwrap_or("")
}
pub(crate) fn get_plural(id: u16) -> &'static str {
let reg = REGISTRY
.read()
.expect("extra unit registry lock poisoned");
reg.units
.get(id as usize)
.map(|u| u.plural)
.unwrap_or("")
}
#[cfg(test)]
pub(crate) fn clear_for_tests() {
let mut reg = REGISTRY
.write()
.expect("extra unit registry lock poisoned");
reg.by_name.clear();
reg.units.clear();
}

View File

@ -10,6 +10,8 @@ use crate::Constant::{E, Pi};
use crate::LexerKeyword::{In, PercentChar, Per, Mercury, Hg, PoundForce, Force, DoubleQuotes, Revolution};
use crate::FunctionIdentifier::{Cbrt, Ceil, Cos, Exp, Abs, Floor, Ln, Log, Round, Sin, Sqrt, Tan};
use crate::units::Unit::*;
use crate::extra_units;
use crate::units::UnitType;
use unicode_segmentation::{Graphemes, UnicodeSegmentation};
fn is_word_char_str(input: &str) -> bool {
@ -591,7 +593,11 @@ fn parse_word(word: &str, lexer: &mut Lexer) -> Result<(), String> {
"f" | "fahrenheit" | "fahrenheits" => Token::Unit(Fahrenheit),
string => {
return Err(format!("Invalid string: {}", string));
if let Some(unit) = extra_units::lookup_unit(string) {
Token::Unit(unit)
} else {
return Err(format!("Invalid string: {}", string));
}
}
};
lexer.tokens.push(token);
@ -903,6 +909,186 @@ pub fn lex(input: &str, remove_trailing_operator: bool) -> Result<Vec<Token>, St
(Token::LexerKeyword(Revolution), Token::LexerKeyword(Per), Token::Unit(Minute)) => {
tokens[token_index-2] = Token::Unit(RevolutionsPerMinute);
},
// composite units:
// - currency per {mass|volume|length|area} (usd/lb, cny/kg, usd/l, usd/m, usd/m2)
// - mass per volume (g/ml, kg/l, lb/gal)
// - volume per mass (ml/g, l/kg)
(Token::Unit(left_u), Token::LexerKeyword(Per), Token::Unit(right_u)) => {
let left_cat = left_u.category();
let right_cat = right_u.category();
if left_cat == UnitType::Currency
&& (right_cat == UnitType::Mass
|| right_cat == UnitType::Volume
|| right_cat == UnitType::Length
|| right_cat == UnitType::Area)
{
let cur_upper = left_u.singular().to_ascii_uppercase();
let cur_lower = cur_upper.to_ascii_lowercase();
let denom_abbrev: &str = match (*right_u, right_cat) {
// Mass
(Milligram, UnitType::Mass) => "mg",
(Gram, UnitType::Mass) => "g",
(Hectogram, UnitType::Mass) => "hg",
(Kilogram, UnitType::Mass) => "kg",
(MetricTon, UnitType::Mass) => "t",
(Ounce, UnitType::Mass) => "oz",
(Pound, UnitType::Mass) => "lb",
(Stone, UnitType::Mass) => "st",
(ShortTon, UnitType::Mass) => "ston",
(LongTon, UnitType::Mass) => "lton",
// Volume
(Milliliter, UnitType::Volume) => "ml",
(Centiliter, UnitType::Volume) => "cl",
(Deciliter, UnitType::Volume) => "dl",
(Liter, UnitType::Volume) => "l",
(Teaspoon, UnitType::Volume) => "tsp",
(Tablespoon, UnitType::Volume) => "tbsp",
(FluidOunce, UnitType::Volume) => "floz",
(Cup, UnitType::Volume) => "cup",
(Pint, UnitType::Volume) => "pt",
(Quart, UnitType::Volume) => "qt",
(Gallon, UnitType::Volume) => "gal",
(OilBarrel, UnitType::Volume) => "bbl",
// Length
(Millimeter, UnitType::Length) => "mm",
(Centimeter, UnitType::Length) => "cm",
(Decimeter, UnitType::Length) => "dm",
(Meter, UnitType::Length) => "m",
(Kilometer, UnitType::Length) => "km",
(Inch, UnitType::Length) => "in",
(Foot, UnitType::Length) => "ft",
(Yard, UnitType::Length) => "yd",
(Mile, UnitType::Length) => "mi",
(Marathon, UnitType::Length) => "marathon",
(NauticalMile, UnitType::Length) => "nmi",
(LightYear, UnitType::Length) => "ly",
(LightSecond, UnitType::Length) => "lightsec",
// Area
(SquareMillimeter, UnitType::Area) => "mm2",
(SquareCentimeter, UnitType::Area) => "cm2",
(SquareDecimeter, UnitType::Area) => "dm2",
(SquareMeter, UnitType::Area) => "m2",
(SquareKilometer, UnitType::Area) => "km2",
(SquareInch, UnitType::Area) => "in2",
(SquareFoot, UnitType::Area) => "ft2",
(SquareYard, UnitType::Area) => "yd2",
(SquareMile, UnitType::Area) => "mi2",
(Are, UnitType::Area) => "a",
(Decare, UnitType::Area) => "daa",
(Hectare, UnitType::Area) => "ha",
(Acre, UnitType::Area) => "acre",
_ => right_u.singular(),
};
let display = format!("{}/{}", cur_upper, denom_abbrev);
let key = format!("{cur_lower}_per_{denom_abbrev}");
// weight = (USD per currency unit) / (base volume-or-mass per denom unit)
let weight = left_u.weight() / right_u.weight();
let composite_type = match right_cat {
UnitType::Mass => UnitType::CurrencyPerMass,
UnitType::Volume => UnitType::CurrencyPerVolume,
UnitType::Length => UnitType::CurrencyPerLength,
UnitType::Area => UnitType::CurrencyPerArea,
_ => {
replaced = false;
UnitType::CurrencyPerMass
}
};
let composite = extra_units::upsert_unit(
&key,
&display,
&display,
composite_type,
weight,
);
tokens[token_index-2] = Token::Unit(composite);
} else if left_cat == UnitType::Mass && right_cat == UnitType::Volume {
let mass_abbrev: &str = match *left_u {
Milligram => "mg",
Gram => "g",
Hectogram => "hg",
Kilogram => "kg",
MetricTon => "t",
Ounce => "oz",
Pound => "lb",
Stone => "st",
ShortTon => "ston",
LongTon => "lton",
_ => left_u.singular(),
};
let vol_abbrev: &str = match *right_u {
Milliliter => "ml",
Centiliter => "cl",
Deciliter => "dl",
Liter => "l",
Teaspoon => "tsp",
Tablespoon => "tbsp",
FluidOunce => "floz",
Cup => "cup",
Pint => "pt",
Quart => "qt",
Gallon => "gal",
OilBarrel => "bbl",
_ => right_u.singular(),
};
let display = format!("{}/{}", mass_abbrev, vol_abbrev);
let key = format!("{mass_abbrev}_per_{vol_abbrev}");
let weight = left_u.weight() / right_u.weight(); // grams per cubic-mm
let composite = extra_units::upsert_unit(
&key,
&display,
&display,
UnitType::MassPerVolume,
weight,
);
tokens[token_index-2] = Token::Unit(composite);
} else if left_cat == UnitType::Volume && right_cat == UnitType::Mass {
let vol_abbrev: &str = match *left_u {
Milliliter => "ml",
Centiliter => "cl",
Deciliter => "dl",
Liter => "l",
Teaspoon => "tsp",
Tablespoon => "tbsp",
FluidOunce => "floz",
Cup => "cup",
Pint => "pt",
Quart => "qt",
Gallon => "gal",
OilBarrel => "bbl",
_ => left_u.singular(),
};
let mass_abbrev: &str = match *right_u {
Milligram => "mg",
Gram => "g",
Hectogram => "hg",
Kilogram => "kg",
MetricTon => "t",
Ounce => "oz",
Pound => "lb",
Stone => "st",
ShortTon => "ston",
LongTon => "lton",
_ => right_u.singular(),
};
let display = format!("{}/{}", vol_abbrev, mass_abbrev);
let key = format!("{vol_abbrev}_per_{mass_abbrev}");
let weight = left_u.weight() / right_u.weight(); // cubic-mm per gram
let composite = extra_units::upsert_unit(
&key,
&display,
&display,
UnitType::VolumePerMass,
weight,
);
tokens[token_index-2] = Token::Unit(composite);
} else {
replaced = false;
}
},
_ => {
replaced = false;
},

View File

@ -34,6 +34,10 @@ pub mod evaluator;
pub mod lexer;
#[rustfmt::skip]
mod lookup;
#[rustfmt::skip]
mod extra_units;
/// Currency rate loading / registration (USD base from exchangerate-api)
pub mod currency;
/// Turns [`Token`]s into an [`AstNode`](parser::AstNode)
pub mod parser;
/// Units, and functions you can use with them
@ -299,6 +303,16 @@ pub fn wasm_eval(expression: &str) -> String {
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn wasm_set_currency_rates(json: &str) -> String {
console_error_panic_hook::set_once();
match currency::register_currencies_from_exchange_rate_api_json(json) {
Ok(n) => format!("OK ({n} currencies)"),
Err(e) => format!("Error: {e}"),
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,4 +1,6 @@
use cpc::eval;
#[cfg(not(target_arch = "wasm32"))]
use cpc::currency;
use std::env;
use std::process::exit;
@ -60,6 +62,12 @@ fn main() {
}
};
// Best-effort: load currency rates so `usd`, `eur`, etc work as extra units.
#[cfg(not(target_arch = "wasm32"))]
{
let _ = currency::init_currencies_blocking();
}
match eval(&expression, true, verbose) {
Ok(answer) => {
if !verbose {

View File

@ -1,5 +1,6 @@
use fastnum::{dec128 as d, D128, D256};
use fastnum::{dec128 as d, D128};
use crate::Number;
use crate::extra_units;
#[derive(Clone, Copy, PartialEq, Debug)]
/// An enum of all possible unit types, like [`Length`], [`DigitalStorage`] etc.
@ -39,6 +40,20 @@ pub enum UnitType {
Speed,
/// A unit of temperature, for example [`Kelvin`]
Temperature,
/// A currency unit, for example USD / EUR (loaded dynamically)
Currency,
/// A currency-per-mass unit, for example USD/lb or CNY/kg
CurrencyPerMass,
/// A currency-per-volume unit, for example USD/l or EUR/ml
CurrencyPerVolume,
/// A currency-per-length unit, for example USD/m or EUR/ft
CurrencyPerLength,
/// A currency-per-area unit, for example USD/m2 or EUR/ft2
CurrencyPerArea,
/// A mass-per-volume unit (density / concentration), e.g. g/ml
MassPerVolume,
/// A volume-per-mass unit (specific volume), e.g. ml/g
VolumePerMass,
}
use UnitType::*;
@ -50,7 +65,9 @@ macro_rules! create_units {
#[derive(Clone, Copy, PartialEq, Debug)]
/// A Unit enum. Note that it can also be [`NoUnit`].
pub enum Unit {
$($variant),*
$($variant),*,
/// Runtime-registered unit (currencies, custom units, etc)
Extra(u16)
}
use Unit::*;
@ -59,28 +76,32 @@ macro_rules! create_units {
match self {
$(
Unit::$variant => $properties.0
),*
),*,
Unit::Extra(id) => extra_units::get_category(*id),
}
}
pub fn weight(&self) -> D128 {
match self {
$(
Unit::$variant => $properties.1
),*
),*,
Unit::Extra(id) => extra_units::get_weight(*id),
}
}
pub(crate) fn singular(&self) -> &str {
match self {
$(
Unit::$variant => $properties.2
),*
),*,
Unit::Extra(id) => extra_units::get_singular(*id),
}
}
pub(crate) fn plural(&self) -> &str {
match self {
$(
Unit::$variant => $properties.3
),*
),*,
Unit::Extra(id) => extra_units::get_plural(*id),
}
}
}
@ -549,6 +570,49 @@ pub fn multiply(left: Number, right: Number) -> Result<Number, String> {
actual_multiply(left, right, false)
}
fn parse_currency_numerator(unit: Unit) -> Option<Unit> {
let code_lower = unit.singular().split('/').next()?.to_ascii_lowercase();
extra_units::lookup_unit(&code_lower)
}
fn parse_mass_numerator(unit: Unit) -> Option<Unit> {
let s = unit.singular().split('/').next()?.to_ascii_lowercase();
let u = match s.as_str() {
"mg" => Milligram,
"g" => Gram,
"hg" => Hectogram,
"kg" => Kilogram,
"t" => MetricTon,
"oz" => Ounce,
"lb" => Pound,
"st" => Stone,
"ston" => ShortTon,
"lton" => LongTon,
_ => return None,
};
Some(u)
}
fn parse_volume_numerator(unit: Unit) -> Option<Unit> {
let s = unit.singular().split('/').next()?.to_ascii_lowercase();
let u = match s.as_str() {
"ml" => Milliliter,
"cl" => Centiliter,
"dl" => Deciliter,
"l" => Liter,
"tsp" => Teaspoon,
"tbsp" => Tablespoon,
"floz" => FluidOunce,
"cup" => Cup,
"pt" => Pint,
"qt" => Quart,
"gal" => Gallon,
"bbl" => OilBarrel,
_ => return None,
};
Some(u)
}
fn actual_multiply(left: Number, right: Number, swapped: bool) -> Result<Number, String> {
let lcat = left.unit.category();
let rcat = right.unit.category();
@ -628,6 +692,81 @@ fn actual_multiply(left: Number, right: Number, swapped: bool) -> Result<Number,
};
let data_storage = Number::new(result, Bit);
Ok(convert(data_storage, final_unit)?)
} else if lcat == CurrencyPerVolume && rcat == Volume {
// (currency per volume) * volume = currency
let rate_usd_per_mm3 = left.value * left.unit.weight();
let volume_mm3 = right.value * right.unit.weight();
let usd_amount = rate_usd_per_mm3 * volume_mm3;
let usd_unit = extra_units::lookup_unit("usd")
.ok_or("Currency rates not initialized (missing USD)")?;
let mut result = Number::new(usd_amount, usd_unit);
if let Some(cur_unit) = parse_currency_numerator(left.unit) {
result = convert(result, cur_unit)?;
}
Ok(result)
} else if lcat == CurrencyPerMass && rcat == Mass {
let rate_usd_per_g = left.value * left.unit.weight();
let grams = right.value * right.unit.weight();
let usd_amount = rate_usd_per_g * grams;
let usd_unit = extra_units::lookup_unit("usd")
.ok_or("Currency rates not initialized (missing USD)")?;
let mut result = Number::new(usd_amount, usd_unit);
if let Some(cur_unit) = parse_currency_numerator(left.unit) {
result = convert(result, cur_unit)?;
}
Ok(result)
} else if lcat == CurrencyPerLength && rcat == Length {
let rate_usd_per_mm = left.value * left.unit.weight();
let mm = right.value * right.unit.weight();
let usd_amount = rate_usd_per_mm * mm;
let usd_unit = extra_units::lookup_unit("usd")
.ok_or("Currency rates not initialized (missing USD)")?;
let mut result = Number::new(usd_amount, usd_unit);
if let Some(cur_unit) = parse_currency_numerator(left.unit) {
result = convert(result, cur_unit)?;
}
Ok(result)
} else if lcat == CurrencyPerArea && rcat == Area {
let rate_usd_per_mm2 = left.value * left.unit.weight();
let mm2 = right.value * right.unit.weight();
let usd_amount = rate_usd_per_mm2 * mm2;
let usd_unit = extra_units::lookup_unit("usd")
.ok_or("Currency rates not initialized (missing USD)")?;
let mut result = Number::new(usd_amount, usd_unit);
if let Some(cur_unit) = parse_currency_numerator(left.unit) {
result = convert(result, cur_unit)?;
}
Ok(result)
} else if lcat == MassPerVolume && rcat == Volume {
// (mass per volume) * volume = mass
let rate_g_per_mm3 = left.value * left.unit.weight();
let volume_mm3 = right.value * right.unit.weight();
let grams = rate_g_per_mm3 * volume_mm3;
let mut result = Number::new(grams, Gram);
if let Some(mass_unit) = parse_mass_numerator(left.unit) {
result = convert(result, mass_unit)?;
}
Ok(result)
} else if lcat == VolumePerMass && rcat == Mass {
// (volume per mass) * mass = volume
let rate_mm3_per_g = left.value * left.unit.weight();
let grams = right.value * right.unit.weight();
let mm3 = rate_mm3_per_g * grams;
let mut result = Number::new(mm3, CubicMillimeter);
if let Some(vol_unit) = parse_volume_numerator(left.unit) {
result = convert(result, vol_unit)?;
}
Ok(result)
} else if lcat == Voltage && rcat == ElectricCurrent {
// 1 volt * 1 ampere = 1 watt
let result = (left.value * left.unit.weight()) * (right.value * right.unit.weight());
@ -712,6 +851,201 @@ pub fn divide(left: Number, right: Number) -> Result<Number, String> {
let bits_per_second = convert(right, BitsPerSecond)?;
let seconds = Number::new(bits.value / bits_per_second.value, Second);
Ok(to_ideal_unit(seconds))
} else if lcat == Currency && rcat == Volume {
// 5 usd / 64 floz => usd/floz (convertible to usd/l, cny/l, etc)
let cur_upper = left.unit.singular().to_ascii_uppercase();
let cur_lower = cur_upper.to_ascii_lowercase();
let denom_abbrev: &str = match right.unit {
Milliliter => "ml",
Centiliter => "cl",
Deciliter => "dl",
Liter => "l",
Teaspoon => "tsp",
Tablespoon => "tbsp",
FluidOunce => "floz",
Cup => "cup",
Pint => "pt",
Quart => "qt",
Gallon => "gal",
OilBarrel => "bbl",
_ => right.unit.singular(),
};
let display = format!("{}/{}", cur_upper, denom_abbrev);
let key = format!("{cur_lower}_per_{denom_abbrev}");
let composite = crate::extra_units::upsert_unit(
&key,
&display,
&display,
CurrencyPerVolume,
left.unit.weight() / right.unit.weight(),
);
Ok(Number::new(left.value / right.value, composite))
} else if lcat == Currency && rcat == Mass {
// usd/kg etc
let cur_upper = left.unit.singular().to_ascii_uppercase();
let cur_lower = cur_upper.to_ascii_lowercase();
let denom_abbrev: &str = match right.unit {
Milligram => "mg",
Gram => "g",
Hectogram => "hg",
Kilogram => "kg",
MetricTon => "t",
Ounce => "oz",
Pound => "lb",
Stone => "st",
ShortTon => "ston",
LongTon => "lton",
_ => right.unit.singular(),
};
let display = format!("{}/{}", cur_upper, denom_abbrev);
let key = format!("{cur_lower}_per_{denom_abbrev}");
let composite = crate::extra_units::upsert_unit(
&key,
&display,
&display,
CurrencyPerMass,
left.unit.weight() / right.unit.weight(),
);
Ok(Number::new(left.value / right.value, composite))
} else if lcat == Currency && rcat == Length {
// usd/m, etc
let cur_upper = left.unit.singular().to_ascii_uppercase();
let cur_lower = cur_upper.to_ascii_lowercase();
let denom_abbrev: &str = match right.unit {
Millimeter => "mm",
Centimeter => "cm",
Decimeter => "dm",
Meter => "m",
Kilometer => "km",
Inch => "in",
Foot => "ft",
Yard => "yd",
Mile => "mi",
NauticalMile => "nmi",
LightYear => "ly",
LightSecond => "lightsec",
_ => right.unit.singular(),
};
let display = format!("{}/{}", cur_upper, denom_abbrev);
let key = format!("{cur_lower}_per_{denom_abbrev}");
let composite = crate::extra_units::upsert_unit(
&key,
&display,
&display,
CurrencyPerLength,
left.unit.weight() / right.unit.weight(),
);
Ok(Number::new(left.value / right.value, composite))
} else if lcat == Currency && rcat == Area {
// usd/m2, etc
let cur_upper = left.unit.singular().to_ascii_uppercase();
let cur_lower = cur_upper.to_ascii_lowercase();
let denom_abbrev: &str = match right.unit {
SquareMillimeter => "mm2",
SquareCentimeter => "cm2",
SquareDecimeter => "dm2",
SquareMeter => "m2",
SquareKilometer => "km2",
SquareInch => "in2",
SquareFoot => "ft2",
SquareYard => "yd2",
SquareMile => "mi2",
Are => "a",
Decare => "daa",
Hectare => "ha",
Acre => "acre",
_ => right.unit.singular(),
};
let display = format!("{}/{}", cur_upper, denom_abbrev);
let key = format!("{cur_lower}_per_{denom_abbrev}");
let composite = crate::extra_units::upsert_unit(
&key,
&display,
&display,
CurrencyPerArea,
left.unit.weight() / right.unit.weight(),
);
Ok(Number::new(left.value / right.value, composite))
} else if lcat == Mass && rcat == Volume {
// 5 g / 64 floz => g/floz (convertible to g/ml, kg/l, etc)
let mass_abbrev: &str = match left.unit {
Milligram => "mg",
Gram => "g",
Hectogram => "hg",
Kilogram => "kg",
MetricTon => "t",
Ounce => "oz",
Pound => "lb",
Stone => "st",
ShortTon => "ston",
LongTon => "lton",
_ => left.unit.singular(),
};
let vol_abbrev: &str = match right.unit {
Milliliter => "ml",
Centiliter => "cl",
Deciliter => "dl",
Liter => "l",
Teaspoon => "tsp",
Tablespoon => "tbsp",
FluidOunce => "floz",
Cup => "cup",
Pint => "pt",
Quart => "qt",
Gallon => "gal",
OilBarrel => "bbl",
_ => right.unit.singular(),
};
let display = format!("{}/{}", mass_abbrev, vol_abbrev);
let key = format!("{mass_abbrev}_per_{vol_abbrev}");
let composite = crate::extra_units::upsert_unit(
&key,
&display,
&display,
MassPerVolume,
left.unit.weight() / right.unit.weight(),
);
Ok(Number::new(left.value / right.value, composite))
} else if lcat == Volume && rcat == Mass {
// ml/g etc
let vol_abbrev: &str = match left.unit {
Milliliter => "ml",
Centiliter => "cl",
Deciliter => "dl",
Liter => "l",
Teaspoon => "tsp",
Tablespoon => "tbsp",
FluidOunce => "floz",
Cup => "cup",
Pint => "pt",
Quart => "qt",
Gallon => "gal",
OilBarrel => "bbl",
_ => left.unit.singular(),
};
let mass_abbrev: &str = match right.unit {
Milligram => "mg",
Gram => "g",
Hectogram => "hg",
Kilogram => "kg",
MetricTon => "t",
Ounce => "oz",
Pound => "lb",
Stone => "st",
ShortTon => "ston",
LongTon => "lton",
_ => right.unit.singular(),
};
let display = format!("{}/{}", vol_abbrev, mass_abbrev);
let key = format!("{vol_abbrev}_per_{mass_abbrev}");
let composite = crate::extra_units::upsert_unit(
&key,
&display,
&display,
VolumePerMass,
left.unit.weight() / right.unit.weight(),
);
Ok(Number::new(left.value / right.value, composite))
} else if lcat == Power && rcat == ElectricCurrent {
// 1 watt / 1 ampere = 1 volt
let result = (left.value * left.unit.weight()) / (right.value * right.unit.weight());
@ -754,15 +1088,6 @@ pub fn modulo(left: Number, right: Number) -> Result<Number, String> {
}
}
fn do_pow(left: D128, right: D128) -> D128 {
// Do pow with d256 for higher accuracy
let left = D256::try_from(left.transmute()).unwrap();
let right = D256::try_from(right.transmute()).unwrap();
let result = left.pow(right);
let result_d128: D128 = result.transmute();
result_d128
}
/// Returns a [`Number`] to the power of another [`Number`]
///
/// - If you take [`Length`] to the power of [`NoType`], the result has a unit of [`Area`].
@ -770,20 +1095,21 @@ fn do_pow(left: D128, right: D128) -> D128 {
/// - If you take [`Length`] to the power of [`Area`], the result has a unit of [`Volume`]
/// - etc.
pub fn pow(left: Number, right: Number) -> Result<Number, String> {
// I tried converting `right` to use powi, but somehow that was slower
let lcat = left.unit.category();
let rcat = left.unit.category();
if left.unit == NoUnit && right.unit == NoUnit {
// 3 ^ 2
Ok(Number::new(do_pow(left.value, right.value), left.unit))
Ok(Number::new(left.value.pow(right.value), left.unit))
} else if right.value == d!(1) && right.unit == NoUnit {
Ok(left)
} else if lcat == Length && right.unit == NoUnit && right.value == d!(2) {
// x km ^ 2
let result = do_pow(left.value * left.unit.weight(), right.value);
let result = (left.value * left.unit.weight()).pow(right.value);
Ok(to_ideal_unit(Number::new(result, SquareMillimeter)))
} else if lcat == Length && right.unit == NoUnit && right.value == d!(3) {
// x km ^ 3
let result = do_pow(left.value * left.unit.weight(), right.value);
let result = (left.value * left.unit.weight()).pow(right.value);
Ok(to_ideal_unit(Number::new(result, CubicMillimeter)))
} else if lcat == Length && rcat == Length && right.value == d!(1) {
// x km ^ 1 km

96
web/package-lock.json generated
View File

@ -10,11 +10,11 @@
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.1.8",
"cpc": "file:../pkg",
"svelte": "^5.33.10",
"svelte": "^5.33.13",
"svelte-check": "^4.2.1",
"tailwindcss": "^4.0.0",
"tailwindcss": "^4.1.8",
"typescript": "^5.8.3",
"vercel": "^42.2.0",
"vercel": "^42.3.0",
"vite": "^6.3.5",
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-wasm": "^3.4.1",
@ -23,7 +23,7 @@
},
"../pkg": {
"name": "cpc",
"version": "2.0.0",
"version": "3.0.0",
"dev": true,
"license": "MIT"
},
@ -1752,9 +1752,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.15.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.27.tgz",
"integrity": "sha512-5fF+eu5mwihV2BeVtX5vijhdaZOfkQTATrePEaXTcKqI16LhJ7gi2/Vhd9OZM0UojcdmiOCVg5rrax+i1MdoQQ==",
"version": "22.15.29",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
"dev": true,
"license": "MIT",
"peer": true,
@ -1984,9 +1984,9 @@
}
},
"node_modules/@vercel/gatsby-plugin-vercel-builder": {
"version": "2.0.82",
"resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.0.82.tgz",
"integrity": "sha512-KlJcfdS+pEjXQ7mmEv67RorxqUwbtRWB7RN8F/nPn9eUaoNCirvfgfO83CJyljWEOoaXcBv3U9rGzs/YtKjF/A==",
"version": "2.0.83",
"resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.0.83.tgz",
"integrity": "sha512-SSuCIHZmTTMc6HEnipkAkW2+cbEmCHGnOyF3uvOfEzjiQX9zB50ziq0DV3yPltSxp37MFkYKDUeWJe2ESXPJaA==",
"dev": true,
"dependencies": {
"@sinclair/typebox": "0.25.24",
@ -2040,13 +2040,13 @@
"license": "Apache-2.0"
},
"node_modules/@vercel/hydrogen": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@vercel/hydrogen/-/hydrogen-1.2.1.tgz",
"integrity": "sha512-pHHiYkvCI+SkRHVDzBsp0HoV3pLymIJIFmRsifCyWryBzhFIg4nA8XUuG2cHGJxOTrnnc85o1Xw6DyagDT34dQ==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vercel/hydrogen/-/hydrogen-1.2.2.tgz",
"integrity": "sha512-PRA3r1/ZRcklGgs/hczprQZ27jX9Avyq/iEbtmzAFNbFovkTlkE0Wy93pVKJfJ4ISCBzBgUSMktX9+6wgjs32A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@vercel/static-config": "3.1.0",
"@vercel/static-config": "3.1.1",
"ts-morph": "12.0.0"
}
},
@ -2088,9 +2088,9 @@
}
},
"node_modules/@vercel/nft": {
"version": "0.29.3",
"resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.3.tgz",
"integrity": "sha512-aVV0E6vJpuvImiMwU1/5QKkw2N96BRFE7mBYGS7FhXUoS6V7SarQ+8tuj33o7ofECz8JtHpmQ9JW+oVzOoB7MA==",
"version": "0.29.4",
"resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.4.tgz",
"integrity": "sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2115,9 +2115,9 @@
}
},
"node_modules/@vercel/node": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.2.0.tgz",
"integrity": "sha512-E8LpLhU13v7tKU+RxXESYtQZDVkt4CepK6uNN7CrzZzEuZqphkC8QrXg1Yrdb9kt8NgJaFjGeBtp9BkiRRC8WA==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.2.1.tgz",
"integrity": "sha512-0+YV01grkqfHIHhmWeCXWmgeP6GsuzXtgWBri3+qESwfAZ6dOTBG4GJp9z2E7sEi+wP60S0/eNGRj8z77uk3JQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -2128,7 +2128,7 @@
"@vercel/build-utils": "10.6.0",
"@vercel/error-utils": "2.0.3",
"@vercel/nft": "0.29.2",
"@vercel/static-config": "3.1.0",
"@vercel/static-config": "3.1.1",
"async-listen": "3.0.0",
"cjs-module-lexer": "1.2.3",
"edge-runtime": "2.5.9",
@ -2267,14 +2267,14 @@
"license": "Apache-2.0"
},
"node_modules/@vercel/redwood": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@vercel/redwood/-/redwood-2.3.2.tgz",
"integrity": "sha512-nAzihiYucoz7nzQNW3DkLnaeyFSujkEI8mEBb5LUM2iUXHCvSIqhh/cCu17KZg10aho/R5YOIWLoPPDMkMYFzA==",
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@vercel/redwood/-/redwood-2.3.3.tgz",
"integrity": "sha512-9Dfith+CYNNt/5Mkrklu7xWroWgSJVR4uh7mwu/2IvuCiJMNa24ReR9xtQNyGFAwAjdeweQ/nHfImz+12ORfpQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@vercel/nft": "0.29.2",
"@vercel/static-config": "3.1.0",
"@vercel/static-config": "3.1.1",
"semver": "6.3.1",
"ts-morph": "12.0.0"
}
@ -2317,15 +2317,15 @@
}
},
"node_modules/@vercel/remix-builder": {
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/@vercel/remix-builder/-/remix-builder-5.4.8.tgz",
"integrity": "sha512-lQql/nohy4CFr+fo+eOAWqh0PSP8dJuwhzlwH6J8QB9nUZd5PvaJg165y5+Dz5I3hBxYfTQCqBvqmMp/NZCzzQ==",
"version": "5.4.9",
"resolved": "https://registry.npmjs.org/@vercel/remix-builder/-/remix-builder-5.4.9.tgz",
"integrity": "sha512-+fWdMjVI6bO0GUBJbw2seBDnLvPi2dd9aBQHVG2TCbJobBPfXgyEMgRWDS+4gjhXn4jLatX4B5C5iJykkeMqNQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@vercel/error-utils": "2.0.3",
"@vercel/nft": "0.29.2",
"@vercel/static-config": "3.1.0",
"@vercel/static-config": "3.1.1",
"path-to-regexp": "6.1.0",
"path-to-regexp-updated": "npm:path-to-regexp@6.3.0",
"ts-morph": "12.0.0"
@ -2366,22 +2366,22 @@
"license": "Apache-2.0"
},
"node_modules/@vercel/static-build": {
"version": "2.7.8",
"resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.7.8.tgz",
"integrity": "sha512-2mx8QVogdtmnVK26wf3mBYwBEK5GVIcxjuBN/JVu0ozX3UskHvEZwf+BJo7J0RzwSh37mR4ERLP90qpCuWLM8g==",
"version": "2.7.9",
"resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.7.9.tgz",
"integrity": "sha512-0AuRrNAE0wntRjZo0CrmQTwvRVO5/7jtcSShgkAKcx2CsPphDjBh2Ah6Kcwb2w9inqJr5Bpg0pxi/Y915hj2oQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@vercel/gatsby-plugin-vercel-analytics": "1.0.11",
"@vercel/gatsby-plugin-vercel-builder": "2.0.82",
"@vercel/static-config": "3.1.0",
"@vercel/gatsby-plugin-vercel-builder": "2.0.83",
"@vercel/static-config": "3.1.1",
"ts-morph": "12.0.0"
}
},
"node_modules/@vercel/static-config": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.1.0.tgz",
"integrity": "sha512-NUUdlTvqmCrg+/Kd/T1yONDym3bMiUJW+wlaZoqgUNMANvmIkO10/lk/2fFRQtGkYaPlnM+TUrL+U2FghccHQg==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.1.1.tgz",
"integrity": "sha512-IRtKnm9N1Uqd2ayIbLPjRtdwcl1GTWvqF1PuEVNm9O43kmoI+m9VpGlW8oga+5LQq1LmJ2Y67zHr7NbjrH1rrw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -5045,9 +5045,9 @@
}
},
"node_modules/svelte": {
"version": "5.33.10",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.10.tgz",
"integrity": "sha512-/yArPQIBoQS2p86LKnvJywOXkVHeEXnFgrDPSxkEfIAEkykopYuy2bF6UUqHG4IbZlJD6OurLxJT8Kn7kTk9WA==",
"version": "5.33.13",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.13.tgz",
"integrity": "sha512-uT3BAPpHGaJqpOgdwJwIK7P4JkBkSS0vylbaRXxQjt1gr+DZ9BiPkhmbZw3ql8LJofUyz5XyrzzQDgQQdfP86Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -5375,23 +5375,23 @@
"license": "MIT"
},
"node_modules/vercel": {
"version": "42.2.0",
"resolved": "https://registry.npmjs.org/vercel/-/vercel-42.2.0.tgz",
"integrity": "sha512-a733x8TmsOagZoYJyVUxMpNI1UOFmp+dw/1/j2iH+ZmkEbYfiEJo96TtqgqPgzX2qF6iEO63i0wDwwFI6aZssA==",
"version": "42.3.0",
"resolved": "https://registry.npmjs.org/vercel/-/vercel-42.3.0.tgz",
"integrity": "sha512-hLiqfcvsjI7IRm5gYIAE7d7wIAqnn797oyDdMkaw76pAQh6aFqvrw4EYcByuxuCSe/5bwck5LNcJ3neXeceGbQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@vercel/build-utils": "10.6.0",
"@vercel/fun": "1.1.6",
"@vercel/go": "3.2.1",
"@vercel/hydrogen": "1.2.1",
"@vercel/hydrogen": "1.2.2",
"@vercel/next": "4.8.0",
"@vercel/node": "5.2.0",
"@vercel/node": "5.2.1",
"@vercel/python": "4.7.2",
"@vercel/redwood": "2.3.2",
"@vercel/remix-builder": "5.4.8",
"@vercel/redwood": "2.3.3",
"@vercel/remix-builder": "5.4.9",
"@vercel/ruby": "2.2.0",
"@vercel/static-build": "2.7.8",
"@vercel/static-build": "2.7.9",
"chokidar": "4.0.0",
"jose": "5.9.6"
},

View File

@ -16,11 +16,11 @@
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.1.8",
"cpc": "file:../pkg",
"svelte": "^5.33.10",
"svelte": "^5.33.13",
"svelte-check": "^4.2.1",
"tailwindcss": "^4.0.0",
"tailwindcss": "^4.1.8",
"typescript": "^5.8.3",
"vercel": "^42.2.0",
"vercel": "^42.3.0",
"vite": "^6.3.5",
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-wasm": "^3.4.1",

View File

@ -1,7 +1,5 @@
<script lang="ts">
import "../app.css";
let { children } = $props();
</script>
{@render children()}
<slot />

View File

@ -1,2 +1 @@
export const ssr = false;
export const prerender = true;

View File

@ -1,28 +1,42 @@
<script lang="ts">
import { check_shortcut } from "$lib/helpers";
import { wasm_eval } from "cpc";
import { flip } from "svelte/animate";
import { fly } from "svelte/transition";
// Has to be dynamically imported for prerendering to work
// https://github.com/sveltejs/svelte/issues/13155
const cpc_promise = import("cpc");
let cpc: typeof import("cpc") | undefined;
cpc_promise.then((mod) => {
cpc = mod;
});
let input = $state("");
let output = $derived.by(() => {
function wasm_eval(input: string) {
if (!cpc || input.trim().length === 0) {
return "";
}
try {
if (input.trim().length === 0) {
return "";
}
return wasm_eval(input);
return cpc.wasm_eval(input);
} catch (e) {
return "";
}
});
}
let input = $state("");
let output = $derived(wasm_eval(input));
let saved_queries: { id: number; in: string; out: string }[] = $state([]);
</script>
<svelte:head>
<title>cpc</title>
<meta
name="description"
content="Text calculator with support for units and conversion"
/>
</svelte:head>
<main class="w-full px-4 lg:px-8 text-base lg:text-lg">
<nav class="flex items-center justify-between py-4 lg:py-6">
<h1 class="text-3xl font-bold text-amber-600 dark:text-amber-400">
cpc
</h1>
<h1 class="text-3xl font-bold text-amber-600 dark:text-amber-400">cpc</h1>
<a
href="https://github.com/probablykasper/cpc"
aria-label="GitHub repository"
@ -46,30 +60,28 @@
>
</a>
</nav>
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
class="border border-gray-500/50 w-full rounded-lg px-3 py-2 outline-none"
bind:value={input}
onkeydown={(e) => {
onkeydown={async (e) => {
const input_el = e.currentTarget;
const input = e.currentTarget.value;
if (check_shortcut(e, "Enter")) {
const input = e.currentTarget.value;
let out;
try {
out = wasm_eval(e.currentTarget.value);
} catch (e) {
out = "";
}
const out = wasm_eval(input)
console.log(out);
saved_queries.unshift({
id: saved_queries.length,
in: input,
out,
});
e.currentTarget.value = "";
input_el.value = "";
output = "";
}
}}
placeholder="10km/h * 1 decade in light seconds"
autofocus
/>
<div class="pt-1 leading-tight">
<div class="px-3 py-2">