diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..da1249b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,8 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true
+
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
new file mode 100644
index 0000000..662c018
--- /dev/null
+++ b/.github/workflows/deploy.yaml
@@ -0,0 +1,21 @@
+name: Deploy
+
+on:
+ push:
+ branches: [main]
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v1
+ with:
+ bun-version: latest
+ - run: bun install
+ - run: bun run build
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./public
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..50efb07
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,16 @@
+name: Test
+
+on:
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v1
+ with:
+ bun-version: latest
+ - run: bun install
+ - run: bun test
diff --git a/.gitignore b/.gitignore
index 5b3333b..9b1ee42 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,175 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
.idea
-.vsocde
\ No newline at end of file
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c72f27e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# web-tools
+
+A small static site comprising various tools for web development and beyond,
+deployed to GitHub Pages.
+
+## Requirements
+
+- [Bun](https://bun.sh) v1.2.0
+- [Typescript](https://www.typescriptlang.org)
+- [Decimal.js](https://mikemcl.github.io/decimal.js/)
+
+The site also makes use of several packages through CDN:
+
+- [Open Props](https://open-props.style/)
+- [HTMX](https://htmx.org/)
+- [Alpine.js](https://alpinejs.dev/)
+
+## Getting started
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run the dev server:
+
+```bash
+bun run dev
+```
+
+To bundle the typescript into javascript:
+
+```bash
+bun run build
+```
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..614dd57
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,34 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "web-tools",
+ "dependencies": {
+ "decimal.js": "^10.6.0",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0",
+ },
+ },
+ },
+ "packages": {
+ "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
+
+ "@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
+
+ "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
+
+ "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
+
+ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
+ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
+
+ "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
+ }
+}
diff --git a/docs/PULL_REQUEST_TEMPLATE/dev_main.md b/docs/PULL_REQUEST_TEMPLATE/dev_main.md
deleted file mode 100644
index 164bf15..0000000
--- a/docs/PULL_REQUEST_TEMPLATE/dev_main.md
+++ /dev/null
@@ -1 +0,0 @@
-# dev-main
\ No newline at end of file
diff --git a/docs/PULL_REQUEST_TEMPLATE/ticket_dev.md b/docs/PULL_REQUEST_TEMPLATE/ticket_dev.md
deleted file mode 100644
index 2aaa344..0000000
--- a/docs/PULL_REQUEST_TEMPLATE/ticket_dev.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# dank-xxxx
-
-Lorem ipsum dolor sit amet, consectetur adipiscing elit.
-
-## Visual Changes
-
-## Code
-
-```javascript
-const rollD = (faces) => Math.floor(Math.random() * faces) + 1;
-```
\ No newline at end of file
diff --git a/examples/accordion.html b/examples/accordion.html
deleted file mode 100644
index 469801e..0000000
--- a/examples/accordion.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
Accordion To Some
-This is the HTML native <details> tag combined with the <summary>
- tag for the heading
-
- Click me to open or close
- This is a heck of a lot of text you might not want displayed by default. If you do want it displayed
- by default you can add the open property to the details tag, and it will be displayed.
-
- A smidgen of CSS has been added to the <summary> tag to make it clear when you
- hover over it that it can be interacted with.
-
\ No newline at end of file
diff --git a/examples/autocomplete.html b/examples/autocomplete.html
deleted file mode 100644
index 69eec93..0000000
--- a/examples/autocomplete.html
+++ /dev/null
@@ -1,13 +0,0 @@
-HTML, you Autocomplete Me
-An autocomplete input using only HTML, specifically the <datalist> tag
-
- Fruits:
-
-
-
- apples
- pears
- oranges
- lemons
-
-
\ No newline at end of file
diff --git a/examples/chip.html b/examples/chip.html
deleted file mode 100644
index 5c74a19..0000000
--- a/examples/chip.html
+++ /dev/null
@@ -1,7 +0,0 @@
-Chip off the Old Block
-A replication of the
- Chip
- ╳
-
- component found in many ui libraries.
-
\ No newline at end of file
diff --git a/examples/colour.html b/examples/colour.html
deleted file mode 100644
index 14c9d30..0000000
--- a/examples/colour.html
+++ /dev/null
@@ -1,6 +0,0 @@
-All the Colours of the Rainbow
-This is just an <input> of type 'color'. No canvas, it's browser native, and you can
- even select a colour that is on the page, which can't be done with a JavaScript approach.
-
- Pick a colour:
-
\ No newline at end of file
diff --git a/examples/dialog.html b/examples/dialog.html
deleted file mode 100644
index f6a91b6..0000000
--- a/examples/dialog.html
+++ /dev/null
@@ -1,19 +0,0 @@
-Start a Dialog
- This is a native HTML element <dialog>. This does require a bit of JavaScript to make
- use of the dialog api, but significantly less than a framework or package would make you use.
-
- Open Dialog*
-
-* This dialog will open with a simple animation if you're using firefox or chrome/chromium.
- Hopefully this will be added to all browsers soon.
-
-
-
\ No newline at end of file
diff --git a/examples/footnote.html b/examples/footnote.html
deleted file mode 100644
index dbcb334..0000000
--- a/examples/footnote.html
+++ /dev/null
@@ -1,16 +0,0 @@
-I Got a Note in my Foot
-A footnote can be
- achieved with a combination of htmx and our friend the
- details tag.
-
-
-
-
-
-
- [1] This <details> tag can be opened from a footnote with this attribute added to it:
-
- _="on click get the next <details/> then toggle @open on it"
-
-
-
\ No newline at end of file
diff --git a/examples/gradient.html b/examples/gradient.html
deleted file mode 100644
index bc2a785..0000000
--- a/examples/gradient.html
+++ /dev/null
@@ -1,9 +0,0 @@
-Making The Gradient
-
- With the linear-gradient property you can make a gradient background for any element. This
- usually doesn't work with text, but when combined with background-clip: text and
- color: transparent you can make headings that will break your eyes.
-
-
-
Richard Of York Gave Battle In Vain
-
\ No newline at end of file
diff --git a/examples/navbar.html b/examples/navbar.html
deleted file mode 100644
index caac975..0000000
--- a/examples/navbar.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
- Nav
-
\ No newline at end of file
diff --git a/examples/new-morphism.html b/examples/new-morphism.html
deleted file mode 100644
index 39c2d8f..0000000
--- a/examples/new-morphism.html
+++ /dev/null
@@ -1,10 +0,0 @@
-New Morphism, New You
-
- You can make hoverable elements have a shadow effect using the hover pseudo-selector and
- setting its box-shadow property with two values.
-
-
\ No newline at end of file
diff --git a/examples/resize.html b/examples/resize.html
deleted file mode 100644
index 3c5f43c..0000000
--- a/examples/resize.html
+++ /dev/null
@@ -1,16 +0,0 @@
-Size Doesn't Matter
-
- With resize you can make any inline element for which the overflow property
- is set to hidden resizable, across either axis or both.
-
-
\ No newline at end of file
diff --git a/examples/switch.html b/examples/switch.html
deleted file mode 100644
index 8415123..0000000
--- a/examples/switch.html
+++ /dev/null
@@ -1,18 +0,0 @@
-Switch it Up
-A modern approach to the switch with piddly
- JS is used to
- toggle between light and dark mode in. This replicates the switch type that is currently only available in safari,
- but hopefully will be added to other browsers soon.
-
-
-
-
-
-
-
- [1] This switch does use a bit of JavaScript to set dark or light mode, but it comes to less
- than 14 lines. It is possible to toggle dark mode on or off with only css as per Coding2GO's video.
- This implementation, while requiring some code, checks if the user has a preference for dark mode and
- defaults the page accordingly while still allowing the user to flip between light and dark.
-
-
\ No newline at end of file
diff --git a/index.html b/index.html
deleted file mode 100644
index 937384f..0000000
--- a/index.html
+++ /dev/null
@@ -1,120 +0,0 @@
-
-
-
-
-
-
-
- Web Components
-
-
-
-
-
-
-
-
-
-
-
-
- Without Frameworks
-
- JavaScript frameworks are great. They allow you to create web apps with rich functionality and complex
- components, but they come at a cost: bloating your countless packages and arcane methods of bundling
- and transpiling.
-
-
- Many of these 'complex' components can be built with just the Holy Trinity™ of web development
- (HTML, CSS and JavaScript). A lot of the time you don't even need JavaScript, or at best only a
- smidgen. With a canny use of only the first two you can create some use functionality without
- installing a single package.
-
-
- When something is too complex for just HTML and CSS to achieve, there still might not be any need for
- javascript (kind of) thanks to a new kid on the block with some old school ideas:
- htmx , and it's friend
- hyperscript . Routin, state management and DOM
- Manipulation without having to write a line of javascript
-
-
- These components are mainly inspired by
- this talk by Kilian Valkhof
- and this video by the
- youtuber Coding2GO, but also other places across the web where I've been able to take inspiration
- from
- others.
-
-
-
-
-
-
- [1] Read: nicked. I've tried to credit where I've taken inspiration from, but if I've
- missed
- something please let me know.
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/index.ts b/index.ts
new file mode 100644
index 0000000..3913b44
--- /dev/null
+++ b/index.ts
@@ -0,0 +1,24 @@
+const PORT = 3000;
+const BASE_PATH = "./public";
+const INDEX = "/index.html";
+
+const serveFile = (path: string) => {
+ const filePath = BASE_PATH + path;
+ const file = Bun.file(filePath);
+ return new Response(file);
+}
+
+Bun.serve({
+ port: PORT,
+ async fetch(req) {
+ console.log(`Serving on port: ${PORT}`);
+ const path = new URL(req.url).pathname;
+ if (path === "/") {
+ return serveFile(INDEX);
+ }
+ return serveFile(path);
+ },
+ error() {
+ return new Response(null, { status: 404 });
+ },
+});
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c8487fa
--- /dev/null
+++ b/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "web-tools",
+ "module": "index.ts",
+ "type": "module",
+ "scripts": {
+ "dev": "bun index.ts",
+ "build": "bun build src/index.ts --outdir=public/static"
+ },
+ "devDependencies": {
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "decimal.js": "^10.6.0"
+ }
+}
diff --git a/public/colour-picker.html b/public/colour-picker.html
new file mode 100644
index 0000000..a87d7e4
--- /dev/null
+++ b/public/colour-picker.html
@@ -0,0 +1,29 @@
+
+ Colour Picker
+
+
+ Pick a colour:
+
+
+
+
+
Colour Values
+
Hex:
+
RGB:
+
HSL:
+
+
diff --git a/public/home.html b/public/home.html
new file mode 100644
index 0000000..f95e432
--- /dev/null
+++ b/public/home.html
@@ -0,0 +1,40 @@
+
+ About Web Tools
+
+ This is a suite of tools made to assist with common tasks when making web
+ apps. This isn't ground breaking stuff, many of these tools are available
+ elsewhere, but I wanted to test my abilities and also have all the tools
+ I commonly use in one place.
+
+
+
+ This site is made with the unholy trinity of the web; HTML; CSS and
+ ECMAScript. There's handful of other packages and tools under the hood
+ that makes it work:
+
+
+
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..30f5a35
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+ Web Tools
+
+
+
+
+
+
+
+
+
+ Web Tools
+ Menu
+
+
+
+ Home
+
+
+
+
+
+ Colour Picker
+
+
+
+
+
+ Timestamps
+
+
+
+
+
+ Numbers
+
+
+
+
+
+ Units
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/numbers.html b/public/numbers.html
new file mode 100644
index 0000000..7d37075
--- /dev/null
+++ b/public/numbers.html
@@ -0,0 +1,44 @@
+
+ Numbers
+
+
+ Scientific Notation Converter
+
+ Enter a number:
+
+
+
+
+
+
+ Convert
+
+ Result:
+
+
+
+
+
diff --git a/public/static/index.js b/public/static/index.js
new file mode 100644
index 0000000..3495e10
--- /dev/null
+++ b/public/static/index.js
@@ -0,0 +1,2997 @@
+// src/timestamps/ecma.ts
+var getHoursFromMilliseconds = (milliseconds) => {
+ return Math.floor(milliseconds / (1000 * 60 * 60));
+};
+var getDaysFromMilliseconds = (milliseconds) => {
+ return Math.floor(milliseconds / (1000 * 60 * 60 * 24));
+};
+var convertEcmaTimestamp = (ecmaTimestamp, ecmaConversionOption) => {
+ if (!ecmaTimestamp || isNaN(ecmaTimestamp)) {
+ return "Please enter a valid timestamp";
+ }
+ const date = new Date(ecmaTimestamp);
+ switch (ecmaConversionOption) {
+ case "iso":
+ return date.toISOString();
+ case "utc":
+ return date.toUTCString();
+ case "locale":
+ return date.toLocaleString();
+ case "seconds":
+ return Math.floor(ecmaTimestamp / 1000);
+ case "hours":
+ return getHoursFromMilliseconds(ecmaTimestamp);
+ case "days":
+ return getDaysFromMilliseconds(ecmaTimestamp);
+ default:
+ return "Invalid conversion option";
+ }
+};
+// src/timestamps/unix.ts
+var getHoursFromSeconds = (seconds) => {
+ return Math.floor(seconds / (60 * 60));
+};
+var getDaysFromSeconds = (seconds) => {
+ return Math.floor(seconds / (60 * 60 * 24));
+};
+var convertUnixTimestamp = (unixTimestamp, conversionOption) => {
+ if (!unixTimestamp || isNaN(unixTimestamp)) {
+ return "Please enter a valid timestamp";
+ }
+ const date = new Date(unixTimestamp * 1000);
+ switch (conversionOption) {
+ case "iso":
+ return date.toISOString();
+ case "utc":
+ return date.toUTCString();
+ case "locale":
+ return date.toLocaleString();
+ case "milliseconds":
+ return date.getTime();
+ case "hours":
+ return getHoursFromSeconds(date.getTime() / 1000);
+ case "days":
+ return getDaysFromSeconds(date.getTime() / 1000);
+ default:
+ return "Invalid conversion option";
+ }
+};
+// src/timestamps/iso-string.ts
+var convertIsoString = (isoString, conversionType) => {
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
+ if (!isoString || !isoDateRegex.test(isoString)) {
+ return "Please enter a valid ISO string";
+ }
+ const date = new Date(isoString);
+ if (isNaN(date.getTime())) {
+ return "Invalid ISO string";
+ }
+ switch (conversionType) {
+ case "unix":
+ return Math.floor(date.getTime() / 1000);
+ case "ecma":
+ return date.getTime();
+ default:
+ return "Invalid conversion type";
+ }
+};
+// src/colour/hex-to-rgb.ts
+var hexToRgb = (hex) => {
+ const red = parseInt(hex.slice(1, 3), 16);
+ const green = parseInt(hex.slice(3, 5), 16);
+ const blue = parseInt(hex.slice(5, 7), 16);
+ return `rgb(${red}, ${green}, ${blue})`;
+};
+// node_modules/decimal.js/decimal.mjs
+/*!
+ * decimal.js v10.6.0
+ * An arbitrary-precision Decimal type for JavaScript.
+ * https://github.com/MikeMcl/decimal.js
+ * Copyright (c) 2025 Michael Mclaughlin
+ * MIT Licence
+ */
+var EXP_LIMIT = 9000000000000000;
+var MAX_DIGITS = 1e9;
+var NUMERALS = "0123456789abcdef";
+var LN10 = "2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862486334095254650828067566662873690987816894829072083255546808437998948262331985283935053089653777326288461633662222876982198867465436674744042432743651550489343149393914796194044002221051017141748003688084012647080685567743216228355220114804663715659121373450747856947683463616792101806445070648000277502684916746550586856935673420670581136429224554405758925724208241314695689016758940256776311356919292033376587141660230105703089634572075440370847469940168269282808481184289314848524948644871927809676271275775397027668605952496716674183485704422507197965004714951050492214776567636938662976979522110718264549734772662425709429322582798502585509785265383207606726317164309505995087807523710333101197857547331541421808427543863591778117054309827482385045648019095610299291824318237525357709750539565187697510374970888692180205189339507238539205144634197265287286965110862571492198849978748873771345686209167058";
+var PI = "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632789";
+var DEFAULTS = {
+ precision: 20,
+ rounding: 4,
+ modulo: 1,
+ toExpNeg: -7,
+ toExpPos: 21,
+ minE: -EXP_LIMIT,
+ maxE: EXP_LIMIT,
+ crypto: false
+};
+var inexact;
+var quadrant;
+var external = true;
+var decimalError = "[DecimalError] ";
+var invalidArgument = decimalError + "Invalid argument: ";
+var precisionLimitExceeded = decimalError + "Precision limit exceeded";
+var cryptoUnavailable = decimalError + "crypto unavailable";
+var tag = "[object Decimal]";
+var mathfloor = Math.floor;
+var mathpow = Math.pow;
+var isBinary = /^0b([01]+(\.[01]*)?|\.[01]+)(p[+-]?\d+)?$/i;
+var isHex = /^0x([0-9a-f]+(\.[0-9a-f]*)?|\.[0-9a-f]+)(p[+-]?\d+)?$/i;
+var isOctal = /^0o([0-7]+(\.[0-7]*)?|\.[0-7]+)(p[+-]?\d+)?$/i;
+var isDecimal = /^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
+var BASE = 1e7;
+var LOG_BASE = 7;
+var MAX_SAFE_INTEGER = 9007199254740991;
+var LN10_PRECISION = LN10.length - 1;
+var PI_PRECISION = PI.length - 1;
+var P = { toStringTag: tag };
+P.absoluteValue = P.abs = function() {
+ var x = new this.constructor(this);
+ if (x.s < 0)
+ x.s = 1;
+ return finalise(x);
+};
+P.ceil = function() {
+ return finalise(new this.constructor(this), this.e + 1, 2);
+};
+P.clampedTo = P.clamp = function(min, max) {
+ var k, x = this, Ctor = x.constructor;
+ min = new Ctor(min);
+ max = new Ctor(max);
+ if (!min.s || !max.s)
+ return new Ctor(NaN);
+ if (min.gt(max))
+ throw Error(invalidArgument + max);
+ k = x.cmp(min);
+ return k < 0 ? min : x.cmp(max) > 0 ? max : new Ctor(x);
+};
+P.comparedTo = P.cmp = function(y) {
+ var i, j, xdL, ydL, x = this, xd = x.d, yd = (y = new x.constructor(y)).d, xs = x.s, ys = y.s;
+ if (!xd || !yd) {
+ return !xs || !ys ? NaN : xs !== ys ? xs : xd === yd ? 0 : !xd ^ xs < 0 ? 1 : -1;
+ }
+ if (!xd[0] || !yd[0])
+ return xd[0] ? xs : yd[0] ? -ys : 0;
+ if (xs !== ys)
+ return xs;
+ if (x.e !== y.e)
+ return x.e > y.e ^ xs < 0 ? 1 : -1;
+ xdL = xd.length;
+ ydL = yd.length;
+ for (i = 0, j = xdL < ydL ? xdL : ydL;i < j; ++i) {
+ if (xd[i] !== yd[i])
+ return xd[i] > yd[i] ^ xs < 0 ? 1 : -1;
+ }
+ return xdL === ydL ? 0 : xdL > ydL ^ xs < 0 ? 1 : -1;
+};
+P.cosine = P.cos = function() {
+ var pr, rm, x = this, Ctor = x.constructor;
+ if (!x.d)
+ return new Ctor(NaN);
+ if (!x.d[0])
+ return new Ctor(1);
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ Ctor.precision = pr + Math.max(x.e, x.sd()) + LOG_BASE;
+ Ctor.rounding = 1;
+ x = cosine(Ctor, toLessThanHalfPi(Ctor, x));
+ Ctor.precision = pr;
+ Ctor.rounding = rm;
+ return finalise(quadrant == 2 || quadrant == 3 ? x.neg() : x, pr, rm, true);
+};
+P.cubeRoot = P.cbrt = function() {
+ var e, m, n, r, rep, s, sd, t, t3, t3plusx, x = this, Ctor = x.constructor;
+ if (!x.isFinite() || x.isZero())
+ return new Ctor(x);
+ external = false;
+ s = x.s * mathpow(x.s * x, 1 / 3);
+ if (!s || Math.abs(s) == 1 / 0) {
+ n = digitsToString(x.d);
+ e = x.e;
+ if (s = (e - n.length + 1) % 3)
+ n += s == 1 || s == -2 ? "0" : "00";
+ s = mathpow(n, 1 / 3);
+ e = mathfloor((e + 1) / 3) - (e % 3 == (e < 0 ? -1 : 2));
+ if (s == 1 / 0) {
+ n = "5e" + e;
+ } else {
+ n = s.toExponential();
+ n = n.slice(0, n.indexOf("e") + 1) + e;
+ }
+ r = new Ctor(n);
+ r.s = x.s;
+ } else {
+ r = new Ctor(s.toString());
+ }
+ sd = (e = Ctor.precision) + 3;
+ for (;; ) {
+ t = r;
+ t3 = t.times(t).times(t);
+ t3plusx = t3.plus(x);
+ r = divide(t3plusx.plus(x).times(t), t3plusx.plus(t3), sd + 2, 1);
+ if (digitsToString(t.d).slice(0, sd) === (n = digitsToString(r.d)).slice(0, sd)) {
+ n = n.slice(sd - 3, sd + 1);
+ if (n == "9999" || !rep && n == "4999") {
+ if (!rep) {
+ finalise(t, e + 1, 0);
+ if (t.times(t).times(t).eq(x)) {
+ r = t;
+ break;
+ }
+ }
+ sd += 4;
+ rep = 1;
+ } else {
+ if (!+n || !+n.slice(1) && n.charAt(0) == "5") {
+ finalise(r, e + 1, 1);
+ m = !r.times(r).times(r).eq(x);
+ }
+ break;
+ }
+ }
+ }
+ external = true;
+ return finalise(r, e, Ctor.rounding, m);
+};
+P.decimalPlaces = P.dp = function() {
+ var w, d = this.d, n = NaN;
+ if (d) {
+ w = d.length - 1;
+ n = (w - mathfloor(this.e / LOG_BASE)) * LOG_BASE;
+ w = d[w];
+ if (w)
+ for (;w % 10 == 0; w /= 10)
+ n--;
+ if (n < 0)
+ n = 0;
+ }
+ return n;
+};
+P.dividedBy = P.div = function(y) {
+ return divide(this, new this.constructor(y));
+};
+P.dividedToIntegerBy = P.divToInt = function(y) {
+ var x = this, Ctor = x.constructor;
+ return finalise(divide(x, new Ctor(y), 0, 1, 1), Ctor.precision, Ctor.rounding);
+};
+P.equals = P.eq = function(y) {
+ return this.cmp(y) === 0;
+};
+P.floor = function() {
+ return finalise(new this.constructor(this), this.e + 1, 3);
+};
+P.greaterThan = P.gt = function(y) {
+ return this.cmp(y) > 0;
+};
+P.greaterThanOrEqualTo = P.gte = function(y) {
+ var k = this.cmp(y);
+ return k == 1 || k === 0;
+};
+P.hyperbolicCosine = P.cosh = function() {
+ var k, n, pr, rm, len, x = this, Ctor = x.constructor, one = new Ctor(1);
+ if (!x.isFinite())
+ return new Ctor(x.s ? 1 / 0 : NaN);
+ if (x.isZero())
+ return one;
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ Ctor.precision = pr + Math.max(x.e, x.sd()) + 4;
+ Ctor.rounding = 1;
+ len = x.d.length;
+ if (len < 32) {
+ k = Math.ceil(len / 3);
+ n = (1 / tinyPow(4, k)).toString();
+ } else {
+ k = 16;
+ n = "2.3283064365386962890625e-10";
+ }
+ x = taylorSeries(Ctor, 1, x.times(n), new Ctor(1), true);
+ var cosh2_x, i = k, d8 = new Ctor(8);
+ for (;i--; ) {
+ cosh2_x = x.times(x);
+ x = one.minus(cosh2_x.times(d8.minus(cosh2_x.times(d8))));
+ }
+ return finalise(x, Ctor.precision = pr, Ctor.rounding = rm, true);
+};
+P.hyperbolicSine = P.sinh = function() {
+ var k, pr, rm, len, x = this, Ctor = x.constructor;
+ if (!x.isFinite() || x.isZero())
+ return new Ctor(x);
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ Ctor.precision = pr + Math.max(x.e, x.sd()) + 4;
+ Ctor.rounding = 1;
+ len = x.d.length;
+ if (len < 3) {
+ x = taylorSeries(Ctor, 2, x, x, true);
+ } else {
+ k = 1.4 * Math.sqrt(len);
+ k = k > 16 ? 16 : k | 0;
+ x = x.times(1 / tinyPow(5, k));
+ x = taylorSeries(Ctor, 2, x, x, true);
+ var sinh2_x, d5 = new Ctor(5), d16 = new Ctor(16), d20 = new Ctor(20);
+ for (;k--; ) {
+ sinh2_x = x.times(x);
+ x = x.times(d5.plus(sinh2_x.times(d16.times(sinh2_x).plus(d20))));
+ }
+ }
+ Ctor.precision = pr;
+ Ctor.rounding = rm;
+ return finalise(x, pr, rm, true);
+};
+P.hyperbolicTangent = P.tanh = function() {
+ var pr, rm, x = this, Ctor = x.constructor;
+ if (!x.isFinite())
+ return new Ctor(x.s);
+ if (x.isZero())
+ return new Ctor(x);
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ Ctor.precision = pr + 7;
+ Ctor.rounding = 1;
+ return divide(x.sinh(), x.cosh(), Ctor.precision = pr, Ctor.rounding = rm);
+};
+P.inverseCosine = P.acos = function() {
+ var x = this, Ctor = x.constructor, k = x.abs().cmp(1), pr = Ctor.precision, rm = Ctor.rounding;
+ if (k !== -1) {
+ return k === 0 ? x.isNeg() ? getPi(Ctor, pr, rm) : new Ctor(0) : new Ctor(NaN);
+ }
+ if (x.isZero())
+ return getPi(Ctor, pr + 4, rm).times(0.5);
+ Ctor.precision = pr + 6;
+ Ctor.rounding = 1;
+ x = new Ctor(1).minus(x).div(x.plus(1)).sqrt().atan();
+ Ctor.precision = pr;
+ Ctor.rounding = rm;
+ return x.times(2);
+};
+P.inverseHyperbolicCosine = P.acosh = function() {
+ var pr, rm, x = this, Ctor = x.constructor;
+ if (x.lte(1))
+ return new Ctor(x.eq(1) ? 0 : NaN);
+ if (!x.isFinite())
+ return new Ctor(x);
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ Ctor.precision = pr + Math.max(Math.abs(x.e), x.sd()) + 4;
+ Ctor.rounding = 1;
+ external = false;
+ x = x.times(x).minus(1).sqrt().plus(x);
+ external = true;
+ Ctor.precision = pr;
+ Ctor.rounding = rm;
+ return x.ln();
+};
+P.inverseHyperbolicSine = P.asinh = function() {
+ var pr, rm, x = this, Ctor = x.constructor;
+ if (!x.isFinite() || x.isZero())
+ return new Ctor(x);
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ Ctor.precision = pr + 2 * Math.max(Math.abs(x.e), x.sd()) + 6;
+ Ctor.rounding = 1;
+ external = false;
+ x = x.times(x).plus(1).sqrt().plus(x);
+ external = true;
+ Ctor.precision = pr;
+ Ctor.rounding = rm;
+ return x.ln();
+};
+P.inverseHyperbolicTangent = P.atanh = function() {
+ var pr, rm, wpr, xsd, x = this, Ctor = x.constructor;
+ if (!x.isFinite())
+ return new Ctor(NaN);
+ if (x.e >= 0)
+ return new Ctor(x.abs().eq(1) ? x.s / 0 : x.isZero() ? x : NaN);
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ xsd = x.sd();
+ if (Math.max(xsd, pr) < 2 * -x.e - 1)
+ return finalise(new Ctor(x), pr, rm, true);
+ Ctor.precision = wpr = xsd - x.e;
+ x = divide(x.plus(1), new Ctor(1).minus(x), wpr + pr, 1);
+ Ctor.precision = pr + 4;
+ Ctor.rounding = 1;
+ x = x.ln();
+ Ctor.precision = pr;
+ Ctor.rounding = rm;
+ return x.times(0.5);
+};
+P.inverseSine = P.asin = function() {
+ var halfPi, k, pr, rm, x = this, Ctor = x.constructor;
+ if (x.isZero())
+ return new Ctor(x);
+ k = x.abs().cmp(1);
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ if (k !== -1) {
+ if (k === 0) {
+ halfPi = getPi(Ctor, pr + 4, rm).times(0.5);
+ halfPi.s = x.s;
+ return halfPi;
+ }
+ return new Ctor(NaN);
+ }
+ Ctor.precision = pr + 6;
+ Ctor.rounding = 1;
+ x = x.div(new Ctor(1).minus(x.times(x)).sqrt().plus(1)).atan();
+ Ctor.precision = pr;
+ Ctor.rounding = rm;
+ return x.times(2);
+};
+P.inverseTangent = P.atan = function() {
+ var i, j, k, n, px, t, r, wpr, x2, x = this, Ctor = x.constructor, pr = Ctor.precision, rm = Ctor.rounding;
+ if (!x.isFinite()) {
+ if (!x.s)
+ return new Ctor(NaN);
+ if (pr + 4 <= PI_PRECISION) {
+ r = getPi(Ctor, pr + 4, rm).times(0.5);
+ r.s = x.s;
+ return r;
+ }
+ } else if (x.isZero()) {
+ return new Ctor(x);
+ } else if (x.abs().eq(1) && pr + 4 <= PI_PRECISION) {
+ r = getPi(Ctor, pr + 4, rm).times(0.25);
+ r.s = x.s;
+ return r;
+ }
+ Ctor.precision = wpr = pr + 10;
+ Ctor.rounding = 1;
+ k = Math.min(28, wpr / LOG_BASE + 2 | 0);
+ for (i = k;i; --i)
+ x = x.div(x.times(x).plus(1).sqrt().plus(1));
+ external = false;
+ j = Math.ceil(wpr / LOG_BASE);
+ n = 1;
+ x2 = x.times(x);
+ r = new Ctor(x);
+ px = x;
+ for (;i !== -1; ) {
+ px = px.times(x2);
+ t = r.minus(px.div(n += 2));
+ px = px.times(x2);
+ r = t.plus(px.div(n += 2));
+ if (r.d[j] !== undefined)
+ for (i = j;r.d[i] === t.d[i] && i--; )
+ ;
+ }
+ if (k)
+ r = r.times(2 << k - 1);
+ external = true;
+ return finalise(r, Ctor.precision = pr, Ctor.rounding = rm, true);
+};
+P.isFinite = function() {
+ return !!this.d;
+};
+P.isInteger = P.isInt = function() {
+ return !!this.d && mathfloor(this.e / LOG_BASE) > this.d.length - 2;
+};
+P.isNaN = function() {
+ return !this.s;
+};
+P.isNegative = P.isNeg = function() {
+ return this.s < 0;
+};
+P.isPositive = P.isPos = function() {
+ return this.s > 0;
+};
+P.isZero = function() {
+ return !!this.d && this.d[0] === 0;
+};
+P.lessThan = P.lt = function(y) {
+ return this.cmp(y) < 0;
+};
+P.lessThanOrEqualTo = P.lte = function(y) {
+ return this.cmp(y) < 1;
+};
+P.logarithm = P.log = function(base) {
+ var isBase10, d, denominator, k, inf, num, sd, r, arg = this, Ctor = arg.constructor, pr = Ctor.precision, rm = Ctor.rounding, guard = 5;
+ if (base == null) {
+ base = new Ctor(10);
+ isBase10 = true;
+ } else {
+ base = new Ctor(base);
+ d = base.d;
+ if (base.s < 0 || !d || !d[0] || base.eq(1))
+ return new Ctor(NaN);
+ isBase10 = base.eq(10);
+ }
+ d = arg.d;
+ if (arg.s < 0 || !d || !d[0] || arg.eq(1)) {
+ return new Ctor(d && !d[0] ? -1 / 0 : arg.s != 1 ? NaN : d ? 0 : 1 / 0);
+ }
+ if (isBase10) {
+ if (d.length > 1) {
+ inf = true;
+ } else {
+ for (k = d[0];k % 10 === 0; )
+ k /= 10;
+ inf = k !== 1;
+ }
+ }
+ external = false;
+ sd = pr + guard;
+ num = naturalLogarithm(arg, sd);
+ denominator = isBase10 ? getLn10(Ctor, sd + 10) : naturalLogarithm(base, sd);
+ r = divide(num, denominator, sd, 1);
+ if (checkRoundingDigits(r.d, k = pr, rm)) {
+ do {
+ sd += 10;
+ num = naturalLogarithm(arg, sd);
+ denominator = isBase10 ? getLn10(Ctor, sd + 10) : naturalLogarithm(base, sd);
+ r = divide(num, denominator, sd, 1);
+ if (!inf) {
+ if (+digitsToString(r.d).slice(k + 1, k + 15) + 1 == 100000000000000) {
+ r = finalise(r, pr + 1, 0);
+ }
+ break;
+ }
+ } while (checkRoundingDigits(r.d, k += 10, rm));
+ }
+ external = true;
+ return finalise(r, pr, rm);
+};
+P.minus = P.sub = function(y) {
+ var d, e, i, j, k, len, pr, rm, xd, xe, xLTy, yd, x = this, Ctor = x.constructor;
+ y = new Ctor(y);
+ if (!x.d || !y.d) {
+ if (!x.s || !y.s)
+ y = new Ctor(NaN);
+ else if (x.d)
+ y.s = -y.s;
+ else
+ y = new Ctor(y.d || x.s !== y.s ? x : NaN);
+ return y;
+ }
+ if (x.s != y.s) {
+ y.s = -y.s;
+ return x.plus(y);
+ }
+ xd = x.d;
+ yd = y.d;
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ if (!xd[0] || !yd[0]) {
+ if (yd[0])
+ y.s = -y.s;
+ else if (xd[0])
+ y = new Ctor(x);
+ else
+ return new Ctor(rm === 3 ? -0 : 0);
+ return external ? finalise(y, pr, rm) : y;
+ }
+ e = mathfloor(y.e / LOG_BASE);
+ xe = mathfloor(x.e / LOG_BASE);
+ xd = xd.slice();
+ k = xe - e;
+ if (k) {
+ xLTy = k < 0;
+ if (xLTy) {
+ d = xd;
+ k = -k;
+ len = yd.length;
+ } else {
+ d = yd;
+ e = xe;
+ len = xd.length;
+ }
+ i = Math.max(Math.ceil(pr / LOG_BASE), len) + 2;
+ if (k > i) {
+ k = i;
+ d.length = 1;
+ }
+ d.reverse();
+ for (i = k;i--; )
+ d.push(0);
+ d.reverse();
+ } else {
+ i = xd.length;
+ len = yd.length;
+ xLTy = i < len;
+ if (xLTy)
+ len = i;
+ for (i = 0;i < len; i++) {
+ if (xd[i] != yd[i]) {
+ xLTy = xd[i] < yd[i];
+ break;
+ }
+ }
+ k = 0;
+ }
+ if (xLTy) {
+ d = xd;
+ xd = yd;
+ yd = d;
+ y.s = -y.s;
+ }
+ len = xd.length;
+ for (i = yd.length - len;i > 0; --i)
+ xd[len++] = 0;
+ for (i = yd.length;i > k; ) {
+ if (xd[--i] < yd[i]) {
+ for (j = i;j && xd[--j] === 0; )
+ xd[j] = BASE - 1;
+ --xd[j];
+ xd[i] += BASE;
+ }
+ xd[i] -= yd[i];
+ }
+ for (;xd[--len] === 0; )
+ xd.pop();
+ for (;xd[0] === 0; xd.shift())
+ --e;
+ if (!xd[0])
+ return new Ctor(rm === 3 ? -0 : 0);
+ y.d = xd;
+ y.e = getBase10Exponent(xd, e);
+ return external ? finalise(y, pr, rm) : y;
+};
+P.modulo = P.mod = function(y) {
+ var q, x = this, Ctor = x.constructor;
+ y = new Ctor(y);
+ if (!x.d || !y.s || y.d && !y.d[0])
+ return new Ctor(NaN);
+ if (!y.d || x.d && !x.d[0]) {
+ return finalise(new Ctor(x), Ctor.precision, Ctor.rounding);
+ }
+ external = false;
+ if (Ctor.modulo == 9) {
+ q = divide(x, y.abs(), 0, 3, 1);
+ q.s *= y.s;
+ } else {
+ q = divide(x, y, 0, Ctor.modulo, 1);
+ }
+ q = q.times(y);
+ external = true;
+ return x.minus(q);
+};
+P.naturalExponential = P.exp = function() {
+ return naturalExponential(this);
+};
+P.naturalLogarithm = P.ln = function() {
+ return naturalLogarithm(this);
+};
+P.negated = P.neg = function() {
+ var x = new this.constructor(this);
+ x.s = -x.s;
+ return finalise(x);
+};
+P.plus = P.add = function(y) {
+ var carry, d, e, i, k, len, pr, rm, xd, yd, x = this, Ctor = x.constructor;
+ y = new Ctor(y);
+ if (!x.d || !y.d) {
+ if (!x.s || !y.s)
+ y = new Ctor(NaN);
+ else if (!x.d)
+ y = new Ctor(y.d || x.s === y.s ? x : NaN);
+ return y;
+ }
+ if (x.s != y.s) {
+ y.s = -y.s;
+ return x.minus(y);
+ }
+ xd = x.d;
+ yd = y.d;
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ if (!xd[0] || !yd[0]) {
+ if (!yd[0])
+ y = new Ctor(x);
+ return external ? finalise(y, pr, rm) : y;
+ }
+ k = mathfloor(x.e / LOG_BASE);
+ e = mathfloor(y.e / LOG_BASE);
+ xd = xd.slice();
+ i = k - e;
+ if (i) {
+ if (i < 0) {
+ d = xd;
+ i = -i;
+ len = yd.length;
+ } else {
+ d = yd;
+ e = k;
+ len = xd.length;
+ }
+ k = Math.ceil(pr / LOG_BASE);
+ len = k > len ? k + 1 : len + 1;
+ if (i > len) {
+ i = len;
+ d.length = 1;
+ }
+ d.reverse();
+ for (;i--; )
+ d.push(0);
+ d.reverse();
+ }
+ len = xd.length;
+ i = yd.length;
+ if (len - i < 0) {
+ i = len;
+ d = yd;
+ yd = xd;
+ xd = d;
+ }
+ for (carry = 0;i; ) {
+ carry = (xd[--i] = xd[i] + yd[i] + carry) / BASE | 0;
+ xd[i] %= BASE;
+ }
+ if (carry) {
+ xd.unshift(carry);
+ ++e;
+ }
+ for (len = xd.length;xd[--len] == 0; )
+ xd.pop();
+ y.d = xd;
+ y.e = getBase10Exponent(xd, e);
+ return external ? finalise(y, pr, rm) : y;
+};
+P.precision = P.sd = function(z) {
+ var k, x = this;
+ if (z !== undefined && z !== !!z && z !== 1 && z !== 0)
+ throw Error(invalidArgument + z);
+ if (x.d) {
+ k = getPrecision(x.d);
+ if (z && x.e + 1 > k)
+ k = x.e + 1;
+ } else {
+ k = NaN;
+ }
+ return k;
+};
+P.round = function() {
+ var x = this, Ctor = x.constructor;
+ return finalise(new Ctor(x), x.e + 1, Ctor.rounding);
+};
+P.sine = P.sin = function() {
+ var pr, rm, x = this, Ctor = x.constructor;
+ if (!x.isFinite())
+ return new Ctor(NaN);
+ if (x.isZero())
+ return new Ctor(x);
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ Ctor.precision = pr + Math.max(x.e, x.sd()) + LOG_BASE;
+ Ctor.rounding = 1;
+ x = sine(Ctor, toLessThanHalfPi(Ctor, x));
+ Ctor.precision = pr;
+ Ctor.rounding = rm;
+ return finalise(quadrant > 2 ? x.neg() : x, pr, rm, true);
+};
+P.squareRoot = P.sqrt = function() {
+ var m, n, sd, r, rep, t, x = this, d = x.d, e = x.e, s = x.s, Ctor = x.constructor;
+ if (s !== 1 || !d || !d[0]) {
+ return new Ctor(!s || s < 0 && (!d || d[0]) ? NaN : d ? x : 1 / 0);
+ }
+ external = false;
+ s = Math.sqrt(+x);
+ if (s == 0 || s == 1 / 0) {
+ n = digitsToString(d);
+ if ((n.length + e) % 2 == 0)
+ n += "0";
+ s = Math.sqrt(n);
+ e = mathfloor((e + 1) / 2) - (e < 0 || e % 2);
+ if (s == 1 / 0) {
+ n = "5e" + e;
+ } else {
+ n = s.toExponential();
+ n = n.slice(0, n.indexOf("e") + 1) + e;
+ }
+ r = new Ctor(n);
+ } else {
+ r = new Ctor(s.toString());
+ }
+ sd = (e = Ctor.precision) + 3;
+ for (;; ) {
+ t = r;
+ r = t.plus(divide(x, t, sd + 2, 1)).times(0.5);
+ if (digitsToString(t.d).slice(0, sd) === (n = digitsToString(r.d)).slice(0, sd)) {
+ n = n.slice(sd - 3, sd + 1);
+ if (n == "9999" || !rep && n == "4999") {
+ if (!rep) {
+ finalise(t, e + 1, 0);
+ if (t.times(t).eq(x)) {
+ r = t;
+ break;
+ }
+ }
+ sd += 4;
+ rep = 1;
+ } else {
+ if (!+n || !+n.slice(1) && n.charAt(0) == "5") {
+ finalise(r, e + 1, 1);
+ m = !r.times(r).eq(x);
+ }
+ break;
+ }
+ }
+ }
+ external = true;
+ return finalise(r, e, Ctor.rounding, m);
+};
+P.tangent = P.tan = function() {
+ var pr, rm, x = this, Ctor = x.constructor;
+ if (!x.isFinite())
+ return new Ctor(NaN);
+ if (x.isZero())
+ return new Ctor(x);
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ Ctor.precision = pr + 10;
+ Ctor.rounding = 1;
+ x = x.sin();
+ x.s = 1;
+ x = divide(x, new Ctor(1).minus(x.times(x)).sqrt(), pr + 10, 0);
+ Ctor.precision = pr;
+ Ctor.rounding = rm;
+ return finalise(quadrant == 2 || quadrant == 4 ? x.neg() : x, pr, rm, true);
+};
+P.times = P.mul = function(y) {
+ var carry, e, i, k, r, rL, t, xdL, ydL, x = this, Ctor = x.constructor, xd = x.d, yd = (y = new Ctor(y)).d;
+ y.s *= x.s;
+ if (!xd || !xd[0] || !yd || !yd[0]) {
+ return new Ctor(!y.s || xd && !xd[0] && !yd || yd && !yd[0] && !xd ? NaN : !xd || !yd ? y.s / 0 : y.s * 0);
+ }
+ e = mathfloor(x.e / LOG_BASE) + mathfloor(y.e / LOG_BASE);
+ xdL = xd.length;
+ ydL = yd.length;
+ if (xdL < ydL) {
+ r = xd;
+ xd = yd;
+ yd = r;
+ rL = xdL;
+ xdL = ydL;
+ ydL = rL;
+ }
+ r = [];
+ rL = xdL + ydL;
+ for (i = rL;i--; )
+ r.push(0);
+ for (i = ydL;--i >= 0; ) {
+ carry = 0;
+ for (k = xdL + i;k > i; ) {
+ t = r[k] + yd[i] * xd[k - i - 1] + carry;
+ r[k--] = t % BASE | 0;
+ carry = t / BASE | 0;
+ }
+ r[k] = (r[k] + carry) % BASE | 0;
+ }
+ for (;!r[--rL]; )
+ r.pop();
+ if (carry)
+ ++e;
+ else
+ r.shift();
+ y.d = r;
+ y.e = getBase10Exponent(r, e);
+ return external ? finalise(y, Ctor.precision, Ctor.rounding) : y;
+};
+P.toBinary = function(sd, rm) {
+ return toStringBinary(this, 2, sd, rm);
+};
+P.toDecimalPlaces = P.toDP = function(dp, rm) {
+ var x = this, Ctor = x.constructor;
+ x = new Ctor(x);
+ if (dp === undefined)
+ return x;
+ checkInt32(dp, 0, MAX_DIGITS);
+ if (rm === undefined)
+ rm = Ctor.rounding;
+ else
+ checkInt32(rm, 0, 8);
+ return finalise(x, dp + x.e + 1, rm);
+};
+P.toExponential = function(dp, rm) {
+ var str, x = this, Ctor = x.constructor;
+ if (dp === undefined) {
+ str = finiteToString(x, true);
+ } else {
+ checkInt32(dp, 0, MAX_DIGITS);
+ if (rm === undefined)
+ rm = Ctor.rounding;
+ else
+ checkInt32(rm, 0, 8);
+ x = finalise(new Ctor(x), dp + 1, rm);
+ str = finiteToString(x, true, dp + 1);
+ }
+ return x.isNeg() && !x.isZero() ? "-" + str : str;
+};
+P.toFixed = function(dp, rm) {
+ var str, y, x = this, Ctor = x.constructor;
+ if (dp === undefined) {
+ str = finiteToString(x);
+ } else {
+ checkInt32(dp, 0, MAX_DIGITS);
+ if (rm === undefined)
+ rm = Ctor.rounding;
+ else
+ checkInt32(rm, 0, 8);
+ y = finalise(new Ctor(x), dp + x.e + 1, rm);
+ str = finiteToString(y, false, dp + y.e + 1);
+ }
+ return x.isNeg() && !x.isZero() ? "-" + str : str;
+};
+P.toFraction = function(maxD) {
+ var d, d0, d1, d2, e, k, n, n0, n1, pr, q, r, x = this, xd = x.d, Ctor = x.constructor;
+ if (!xd)
+ return new Ctor(x);
+ n1 = d0 = new Ctor(1);
+ d1 = n0 = new Ctor(0);
+ d = new Ctor(d1);
+ e = d.e = getPrecision(xd) - x.e - 1;
+ k = e % LOG_BASE;
+ d.d[0] = mathpow(10, k < 0 ? LOG_BASE + k : k);
+ if (maxD == null) {
+ maxD = e > 0 ? d : n1;
+ } else {
+ n = new Ctor(maxD);
+ if (!n.isInt() || n.lt(n1))
+ throw Error(invalidArgument + n);
+ maxD = n.gt(d) ? e > 0 ? d : n1 : n;
+ }
+ external = false;
+ n = new Ctor(digitsToString(xd));
+ pr = Ctor.precision;
+ Ctor.precision = e = xd.length * LOG_BASE * 2;
+ for (;; ) {
+ q = divide(n, d, 0, 1, 1);
+ d2 = d0.plus(q.times(d1));
+ if (d2.cmp(maxD) == 1)
+ break;
+ d0 = d1;
+ d1 = d2;
+ d2 = n1;
+ n1 = n0.plus(q.times(d2));
+ n0 = d2;
+ d2 = d;
+ d = n.minus(q.times(d2));
+ n = d2;
+ }
+ d2 = divide(maxD.minus(d0), d1, 0, 1, 1);
+ n0 = n0.plus(d2.times(n1));
+ d0 = d0.plus(d2.times(d1));
+ n0.s = n1.s = x.s;
+ r = divide(n1, d1, e, 1).minus(x).abs().cmp(divide(n0, d0, e, 1).minus(x).abs()) < 1 ? [n1, d1] : [n0, d0];
+ Ctor.precision = pr;
+ external = true;
+ return r;
+};
+P.toHexadecimal = P.toHex = function(sd, rm) {
+ return toStringBinary(this, 16, sd, rm);
+};
+P.toNearest = function(y, rm) {
+ var x = this, Ctor = x.constructor;
+ x = new Ctor(x);
+ if (y == null) {
+ if (!x.d)
+ return x;
+ y = new Ctor(1);
+ rm = Ctor.rounding;
+ } else {
+ y = new Ctor(y);
+ if (rm === undefined) {
+ rm = Ctor.rounding;
+ } else {
+ checkInt32(rm, 0, 8);
+ }
+ if (!x.d)
+ return y.s ? x : y;
+ if (!y.d) {
+ if (y.s)
+ y.s = x.s;
+ return y;
+ }
+ }
+ if (y.d[0]) {
+ external = false;
+ x = divide(x, y, 0, rm, 1).times(y);
+ external = true;
+ finalise(x);
+ } else {
+ y.s = x.s;
+ x = y;
+ }
+ return x;
+};
+P.toNumber = function() {
+ return +this;
+};
+P.toOctal = function(sd, rm) {
+ return toStringBinary(this, 8, sd, rm);
+};
+P.toPower = P.pow = function(y) {
+ var e, k, pr, r, rm, s, x = this, Ctor = x.constructor, yn = +(y = new Ctor(y));
+ if (!x.d || !y.d || !x.d[0] || !y.d[0])
+ return new Ctor(mathpow(+x, yn));
+ x = new Ctor(x);
+ if (x.eq(1))
+ return x;
+ pr = Ctor.precision;
+ rm = Ctor.rounding;
+ if (y.eq(1))
+ return finalise(x, pr, rm);
+ e = mathfloor(y.e / LOG_BASE);
+ if (e >= y.d.length - 1 && (k = yn < 0 ? -yn : yn) <= MAX_SAFE_INTEGER) {
+ r = intPow(Ctor, x, k, pr);
+ return y.s < 0 ? new Ctor(1).div(r) : finalise(r, pr, rm);
+ }
+ s = x.s;
+ if (s < 0) {
+ if (e < y.d.length - 1)
+ return new Ctor(NaN);
+ if ((y.d[e] & 1) == 0)
+ s = 1;
+ if (x.e == 0 && x.d[0] == 1 && x.d.length == 1) {
+ x.s = s;
+ return x;
+ }
+ }
+ k = mathpow(+x, yn);
+ e = k == 0 || !isFinite(k) ? mathfloor(yn * (Math.log("0." + digitsToString(x.d)) / Math.LN10 + x.e + 1)) : new Ctor(k + "").e;
+ if (e > Ctor.maxE + 1 || e < Ctor.minE - 1)
+ return new Ctor(e > 0 ? s / 0 : 0);
+ external = false;
+ Ctor.rounding = x.s = 1;
+ k = Math.min(12, (e + "").length);
+ r = naturalExponential(y.times(naturalLogarithm(x, pr + k)), pr);
+ if (r.d) {
+ r = finalise(r, pr + 5, 1);
+ if (checkRoundingDigits(r.d, pr, rm)) {
+ e = pr + 10;
+ r = finalise(naturalExponential(y.times(naturalLogarithm(x, e + k)), e), e + 5, 1);
+ if (+digitsToString(r.d).slice(pr + 1, pr + 15) + 1 == 100000000000000) {
+ r = finalise(r, pr + 1, 0);
+ }
+ }
+ }
+ r.s = s;
+ external = true;
+ Ctor.rounding = rm;
+ return finalise(r, pr, rm);
+};
+P.toPrecision = function(sd, rm) {
+ var str, x = this, Ctor = x.constructor;
+ if (sd === undefined) {
+ str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos);
+ } else {
+ checkInt32(sd, 1, MAX_DIGITS);
+ if (rm === undefined)
+ rm = Ctor.rounding;
+ else
+ checkInt32(rm, 0, 8);
+ x = finalise(new Ctor(x), sd, rm);
+ str = finiteToString(x, sd <= x.e || x.e <= Ctor.toExpNeg, sd);
+ }
+ return x.isNeg() && !x.isZero() ? "-" + str : str;
+};
+P.toSignificantDigits = P.toSD = function(sd, rm) {
+ var x = this, Ctor = x.constructor;
+ if (sd === undefined) {
+ sd = Ctor.precision;
+ rm = Ctor.rounding;
+ } else {
+ checkInt32(sd, 1, MAX_DIGITS);
+ if (rm === undefined)
+ rm = Ctor.rounding;
+ else
+ checkInt32(rm, 0, 8);
+ }
+ return finalise(new Ctor(x), sd, rm);
+};
+P.toString = function() {
+ var x = this, Ctor = x.constructor, str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos);
+ return x.isNeg() && !x.isZero() ? "-" + str : str;
+};
+P.truncated = P.trunc = function() {
+ return finalise(new this.constructor(this), this.e + 1, 1);
+};
+P.valueOf = P.toJSON = function() {
+ var x = this, Ctor = x.constructor, str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos);
+ return x.isNeg() ? "-" + str : str;
+};
+function digitsToString(d) {
+ var i, k, ws, indexOfLastWord = d.length - 1, str = "", w = d[0];
+ if (indexOfLastWord > 0) {
+ str += w;
+ for (i = 1;i < indexOfLastWord; i++) {
+ ws = d[i] + "";
+ k = LOG_BASE - ws.length;
+ if (k)
+ str += getZeroString(k);
+ str += ws;
+ }
+ w = d[i];
+ ws = w + "";
+ k = LOG_BASE - ws.length;
+ if (k)
+ str += getZeroString(k);
+ } else if (w === 0) {
+ return "0";
+ }
+ for (;w % 10 === 0; )
+ w /= 10;
+ return str + w;
+}
+function checkInt32(i, min, max) {
+ if (i !== ~~i || i < min || i > max) {
+ throw Error(invalidArgument + i);
+ }
+}
+function checkRoundingDigits(d, i, rm, repeating) {
+ var di, k, r, rd;
+ for (k = d[0];k >= 10; k /= 10)
+ --i;
+ if (--i < 0) {
+ i += LOG_BASE;
+ di = 0;
+ } else {
+ di = Math.ceil((i + 1) / LOG_BASE);
+ i %= LOG_BASE;
+ }
+ k = mathpow(10, LOG_BASE - i);
+ rd = d[di] % k | 0;
+ if (repeating == null) {
+ if (i < 3) {
+ if (i == 0)
+ rd = rd / 100 | 0;
+ else if (i == 1)
+ rd = rd / 10 | 0;
+ r = rm < 4 && rd == 99999 || rm > 3 && rd == 49999 || rd == 50000 || rd == 0;
+ } else {
+ r = (rm < 4 && rd + 1 == k || rm > 3 && rd + 1 == k / 2) && (d[di + 1] / k / 100 | 0) == mathpow(10, i - 2) - 1 || (rd == k / 2 || rd == 0) && (d[di + 1] / k / 100 | 0) == 0;
+ }
+ } else {
+ if (i < 4) {
+ if (i == 0)
+ rd = rd / 1000 | 0;
+ else if (i == 1)
+ rd = rd / 100 | 0;
+ else if (i == 2)
+ rd = rd / 10 | 0;
+ r = (repeating || rm < 4) && rd == 9999 || !repeating && rm > 3 && rd == 4999;
+ } else {
+ r = ((repeating || rm < 4) && rd + 1 == k || !repeating && rm > 3 && rd + 1 == k / 2) && (d[di + 1] / k / 1000 | 0) == mathpow(10, i - 3) - 1;
+ }
+ }
+ return r;
+}
+function convertBase(str, baseIn, baseOut) {
+ var j, arr = [0], arrL, i = 0, strL = str.length;
+ for (;i < strL; ) {
+ for (arrL = arr.length;arrL--; )
+ arr[arrL] *= baseIn;
+ arr[0] += NUMERALS.indexOf(str.charAt(i++));
+ for (j = 0;j < arr.length; j++) {
+ if (arr[j] > baseOut - 1) {
+ if (arr[j + 1] === undefined)
+ arr[j + 1] = 0;
+ arr[j + 1] += arr[j] / baseOut | 0;
+ arr[j] %= baseOut;
+ }
+ }
+ }
+ return arr.reverse();
+}
+function cosine(Ctor, x) {
+ var k, len, y;
+ if (x.isZero())
+ return x;
+ len = x.d.length;
+ if (len < 32) {
+ k = Math.ceil(len / 3);
+ y = (1 / tinyPow(4, k)).toString();
+ } else {
+ k = 16;
+ y = "2.3283064365386962890625e-10";
+ }
+ Ctor.precision += k;
+ x = taylorSeries(Ctor, 1, x.times(y), new Ctor(1));
+ for (var i = k;i--; ) {
+ var cos2x = x.times(x);
+ x = cos2x.times(cos2x).minus(cos2x).times(8).plus(1);
+ }
+ Ctor.precision -= k;
+ return x;
+}
+var divide = function() {
+ function multiplyInteger(x, k, base) {
+ var temp, carry = 0, i = x.length;
+ for (x = x.slice();i--; ) {
+ temp = x[i] * k + carry;
+ x[i] = temp % base | 0;
+ carry = temp / base | 0;
+ }
+ if (carry)
+ x.unshift(carry);
+ return x;
+ }
+ function compare(a, b, aL, bL) {
+ var i, r;
+ if (aL != bL) {
+ r = aL > bL ? 1 : -1;
+ } else {
+ for (i = r = 0;i < aL; i++) {
+ if (a[i] != b[i]) {
+ r = a[i] > b[i] ? 1 : -1;
+ break;
+ }
+ }
+ }
+ return r;
+ }
+ function subtract(a, b, aL, base) {
+ var i = 0;
+ for (;aL--; ) {
+ a[aL] -= i;
+ i = a[aL] < b[aL] ? 1 : 0;
+ a[aL] = i * base + a[aL] - b[aL];
+ }
+ for (;!a[0] && a.length > 1; )
+ a.shift();
+ }
+ return function(x, y, pr, rm, dp, base) {
+ var cmp, e, i, k, logBase, more, prod, prodL, q, qd, rem, remL, rem0, sd, t, xi, xL, yd0, yL, yz, Ctor = x.constructor, sign = x.s == y.s ? 1 : -1, xd = x.d, yd = y.d;
+ if (!xd || !xd[0] || !yd || !yd[0]) {
+ return new Ctor(!x.s || !y.s || (xd ? yd && xd[0] == yd[0] : !yd) ? NaN : xd && xd[0] == 0 || !yd ? sign * 0 : sign / 0);
+ }
+ if (base) {
+ logBase = 1;
+ e = x.e - y.e;
+ } else {
+ base = BASE;
+ logBase = LOG_BASE;
+ e = mathfloor(x.e / logBase) - mathfloor(y.e / logBase);
+ }
+ yL = yd.length;
+ xL = xd.length;
+ q = new Ctor(sign);
+ qd = q.d = [];
+ for (i = 0;yd[i] == (xd[i] || 0); i++)
+ ;
+ if (yd[i] > (xd[i] || 0))
+ e--;
+ if (pr == null) {
+ sd = pr = Ctor.precision;
+ rm = Ctor.rounding;
+ } else if (dp) {
+ sd = pr + (x.e - y.e) + 1;
+ } else {
+ sd = pr;
+ }
+ if (sd < 0) {
+ qd.push(1);
+ more = true;
+ } else {
+ sd = sd / logBase + 2 | 0;
+ i = 0;
+ if (yL == 1) {
+ k = 0;
+ yd = yd[0];
+ sd++;
+ for (;(i < xL || k) && sd--; i++) {
+ t = k * base + (xd[i] || 0);
+ qd[i] = t / yd | 0;
+ k = t % yd | 0;
+ }
+ more = k || i < xL;
+ } else {
+ k = base / (yd[0] + 1) | 0;
+ if (k > 1) {
+ yd = multiplyInteger(yd, k, base);
+ xd = multiplyInteger(xd, k, base);
+ yL = yd.length;
+ xL = xd.length;
+ }
+ xi = yL;
+ rem = xd.slice(0, yL);
+ remL = rem.length;
+ for (;remL < yL; )
+ rem[remL++] = 0;
+ yz = yd.slice();
+ yz.unshift(0);
+ yd0 = yd[0];
+ if (yd[1] >= base / 2)
+ ++yd0;
+ do {
+ k = 0;
+ cmp = compare(yd, rem, yL, remL);
+ if (cmp < 0) {
+ rem0 = rem[0];
+ if (yL != remL)
+ rem0 = rem0 * base + (rem[1] || 0);
+ k = rem0 / yd0 | 0;
+ if (k > 1) {
+ if (k >= base)
+ k = base - 1;
+ prod = multiplyInteger(yd, k, base);
+ prodL = prod.length;
+ remL = rem.length;
+ cmp = compare(prod, rem, prodL, remL);
+ if (cmp == 1) {
+ k--;
+ subtract(prod, yL < prodL ? yz : yd, prodL, base);
+ }
+ } else {
+ if (k == 0)
+ cmp = k = 1;
+ prod = yd.slice();
+ }
+ prodL = prod.length;
+ if (prodL < remL)
+ prod.unshift(0);
+ subtract(rem, prod, remL, base);
+ if (cmp == -1) {
+ remL = rem.length;
+ cmp = compare(yd, rem, yL, remL);
+ if (cmp < 1) {
+ k++;
+ subtract(rem, yL < remL ? yz : yd, remL, base);
+ }
+ }
+ remL = rem.length;
+ } else if (cmp === 0) {
+ k++;
+ rem = [0];
+ }
+ qd[i++] = k;
+ if (cmp && rem[0]) {
+ rem[remL++] = xd[xi] || 0;
+ } else {
+ rem = [xd[xi]];
+ remL = 1;
+ }
+ } while ((xi++ < xL || rem[0] !== undefined) && sd--);
+ more = rem[0] !== undefined;
+ }
+ if (!qd[0])
+ qd.shift();
+ }
+ if (logBase == 1) {
+ q.e = e;
+ inexact = more;
+ } else {
+ for (i = 1, k = qd[0];k >= 10; k /= 10)
+ i++;
+ q.e = i + e * logBase - 1;
+ finalise(q, dp ? pr + q.e + 1 : pr, rm, more);
+ }
+ return q;
+ };
+}();
+function finalise(x, sd, rm, isTruncated) {
+ var digits, i, j, k, rd, roundUp, w, xd, xdi, Ctor = x.constructor;
+ out:
+ if (sd != null) {
+ xd = x.d;
+ if (!xd)
+ return x;
+ for (digits = 1, k = xd[0];k >= 10; k /= 10)
+ digits++;
+ i = sd - digits;
+ if (i < 0) {
+ i += LOG_BASE;
+ j = sd;
+ w = xd[xdi = 0];
+ rd = w / mathpow(10, digits - j - 1) % 10 | 0;
+ } else {
+ xdi = Math.ceil((i + 1) / LOG_BASE);
+ k = xd.length;
+ if (xdi >= k) {
+ if (isTruncated) {
+ for (;k++ <= xdi; )
+ xd.push(0);
+ w = rd = 0;
+ digits = 1;
+ i %= LOG_BASE;
+ j = i - LOG_BASE + 1;
+ } else {
+ break out;
+ }
+ } else {
+ w = k = xd[xdi];
+ for (digits = 1;k >= 10; k /= 10)
+ digits++;
+ i %= LOG_BASE;
+ j = i - LOG_BASE + digits;
+ rd = j < 0 ? 0 : w / mathpow(10, digits - j - 1) % 10 | 0;
+ }
+ }
+ isTruncated = isTruncated || sd < 0 || xd[xdi + 1] !== undefined || (j < 0 ? w : w % mathpow(10, digits - j - 1));
+ roundUp = rm < 4 ? (rd || isTruncated) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) : rd > 5 || rd == 5 && (rm == 4 || isTruncated || rm == 6 && (i > 0 ? j > 0 ? w / mathpow(10, digits - j) : 0 : xd[xdi - 1]) % 10 & 1 || rm == (x.s < 0 ? 8 : 7));
+ if (sd < 1 || !xd[0]) {
+ xd.length = 0;
+ if (roundUp) {
+ sd -= x.e + 1;
+ xd[0] = mathpow(10, (LOG_BASE - sd % LOG_BASE) % LOG_BASE);
+ x.e = -sd || 0;
+ } else {
+ xd[0] = x.e = 0;
+ }
+ return x;
+ }
+ if (i == 0) {
+ xd.length = xdi;
+ k = 1;
+ xdi--;
+ } else {
+ xd.length = xdi + 1;
+ k = mathpow(10, LOG_BASE - i);
+ xd[xdi] = j > 0 ? (w / mathpow(10, digits - j) % mathpow(10, j) | 0) * k : 0;
+ }
+ if (roundUp) {
+ for (;; ) {
+ if (xdi == 0) {
+ for (i = 1, j = xd[0];j >= 10; j /= 10)
+ i++;
+ j = xd[0] += k;
+ for (k = 1;j >= 10; j /= 10)
+ k++;
+ if (i != k) {
+ x.e++;
+ if (xd[0] == BASE)
+ xd[0] = 1;
+ }
+ break;
+ } else {
+ xd[xdi] += k;
+ if (xd[xdi] != BASE)
+ break;
+ xd[xdi--] = 0;
+ k = 1;
+ }
+ }
+ }
+ for (i = xd.length;xd[--i] === 0; )
+ xd.pop();
+ }
+ if (external) {
+ if (x.e > Ctor.maxE) {
+ x.d = null;
+ x.e = NaN;
+ } else if (x.e < Ctor.minE) {
+ x.e = 0;
+ x.d = [0];
+ }
+ }
+ return x;
+}
+function finiteToString(x, isExp, sd) {
+ if (!x.isFinite())
+ return nonFiniteToString(x);
+ var k, e = x.e, str = digitsToString(x.d), len = str.length;
+ if (isExp) {
+ if (sd && (k = sd - len) > 0) {
+ str = str.charAt(0) + "." + str.slice(1) + getZeroString(k);
+ } else if (len > 1) {
+ str = str.charAt(0) + "." + str.slice(1);
+ }
+ str = str + (x.e < 0 ? "e" : "e+") + x.e;
+ } else if (e < 0) {
+ str = "0." + getZeroString(-e - 1) + str;
+ if (sd && (k = sd - len) > 0)
+ str += getZeroString(k);
+ } else if (e >= len) {
+ str += getZeroString(e + 1 - len);
+ if (sd && (k = sd - e - 1) > 0)
+ str = str + "." + getZeroString(k);
+ } else {
+ if ((k = e + 1) < len)
+ str = str.slice(0, k) + "." + str.slice(k);
+ if (sd && (k = sd - len) > 0) {
+ if (e + 1 === len)
+ str += ".";
+ str += getZeroString(k);
+ }
+ }
+ return str;
+}
+function getBase10Exponent(digits, e) {
+ var w = digits[0];
+ for (e *= LOG_BASE;w >= 10; w /= 10)
+ e++;
+ return e;
+}
+function getLn10(Ctor, sd, pr) {
+ if (sd > LN10_PRECISION) {
+ external = true;
+ if (pr)
+ Ctor.precision = pr;
+ throw Error(precisionLimitExceeded);
+ }
+ return finalise(new Ctor(LN10), sd, 1, true);
+}
+function getPi(Ctor, sd, rm) {
+ if (sd > PI_PRECISION)
+ throw Error(precisionLimitExceeded);
+ return finalise(new Ctor(PI), sd, rm, true);
+}
+function getPrecision(digits) {
+ var w = digits.length - 1, len = w * LOG_BASE + 1;
+ w = digits[w];
+ if (w) {
+ for (;w % 10 == 0; w /= 10)
+ len--;
+ for (w = digits[0];w >= 10; w /= 10)
+ len++;
+ }
+ return len;
+}
+function getZeroString(k) {
+ var zs = "";
+ for (;k--; )
+ zs += "0";
+ return zs;
+}
+function intPow(Ctor, x, n, pr) {
+ var isTruncated, r = new Ctor(1), k = Math.ceil(pr / LOG_BASE + 4);
+ external = false;
+ for (;; ) {
+ if (n % 2) {
+ r = r.times(x);
+ if (truncate(r.d, k))
+ isTruncated = true;
+ }
+ n = mathfloor(n / 2);
+ if (n === 0) {
+ n = r.d.length - 1;
+ if (isTruncated && r.d[n] === 0)
+ ++r.d[n];
+ break;
+ }
+ x = x.times(x);
+ truncate(x.d, k);
+ }
+ external = true;
+ return r;
+}
+function isOdd(n) {
+ return n.d[n.d.length - 1] & 1;
+}
+function maxOrMin(Ctor, args, n) {
+ var k, y, x = new Ctor(args[0]), i = 0;
+ for (;++i < args.length; ) {
+ y = new Ctor(args[i]);
+ if (!y.s) {
+ x = y;
+ break;
+ }
+ k = x.cmp(y);
+ if (k === n || k === 0 && x.s === n) {
+ x = y;
+ }
+ }
+ return x;
+}
+function naturalExponential(x, sd) {
+ var denominator, guard, j, pow, sum, t, wpr, rep = 0, i = 0, k = 0, Ctor = x.constructor, rm = Ctor.rounding, pr = Ctor.precision;
+ if (!x.d || !x.d[0] || x.e > 17) {
+ return new Ctor(x.d ? !x.d[0] ? 1 : x.s < 0 ? 0 : 1 / 0 : x.s ? x.s < 0 ? 0 : x : 0 / 0);
+ }
+ if (sd == null) {
+ external = false;
+ wpr = pr;
+ } else {
+ wpr = sd;
+ }
+ t = new Ctor(0.03125);
+ while (x.e > -2) {
+ x = x.times(t);
+ k += 5;
+ }
+ guard = Math.log(mathpow(2, k)) / Math.LN10 * 2 + 5 | 0;
+ wpr += guard;
+ denominator = pow = sum = new Ctor(1);
+ Ctor.precision = wpr;
+ for (;; ) {
+ pow = finalise(pow.times(x), wpr, 1);
+ denominator = denominator.times(++i);
+ t = sum.plus(divide(pow, denominator, wpr, 1));
+ if (digitsToString(t.d).slice(0, wpr) === digitsToString(sum.d).slice(0, wpr)) {
+ j = k;
+ while (j--)
+ sum = finalise(sum.times(sum), wpr, 1);
+ if (sd == null) {
+ if (rep < 3 && checkRoundingDigits(sum.d, wpr - guard, rm, rep)) {
+ Ctor.precision = wpr += 10;
+ denominator = pow = t = new Ctor(1);
+ i = 0;
+ rep++;
+ } else {
+ return finalise(sum, Ctor.precision = pr, rm, external = true);
+ }
+ } else {
+ Ctor.precision = pr;
+ return sum;
+ }
+ }
+ sum = t;
+ }
+}
+function naturalLogarithm(y, sd) {
+ var c, c0, denominator, e, numerator, rep, sum, t, wpr, x1, x2, n = 1, guard = 10, x = y, xd = x.d, Ctor = x.constructor, rm = Ctor.rounding, pr = Ctor.precision;
+ if (x.s < 0 || !xd || !xd[0] || !x.e && xd[0] == 1 && xd.length == 1) {
+ return new Ctor(xd && !xd[0] ? -1 / 0 : x.s != 1 ? NaN : xd ? 0 : x);
+ }
+ if (sd == null) {
+ external = false;
+ wpr = pr;
+ } else {
+ wpr = sd;
+ }
+ Ctor.precision = wpr += guard;
+ c = digitsToString(xd);
+ c0 = c.charAt(0);
+ if (Math.abs(e = x.e) < 1500000000000000) {
+ while (c0 < 7 && c0 != 1 || c0 == 1 && c.charAt(1) > 3) {
+ x = x.times(y);
+ c = digitsToString(x.d);
+ c0 = c.charAt(0);
+ n++;
+ }
+ e = x.e;
+ if (c0 > 1) {
+ x = new Ctor("0." + c);
+ e++;
+ } else {
+ x = new Ctor(c0 + "." + c.slice(1));
+ }
+ } else {
+ t = getLn10(Ctor, wpr + 2, pr).times(e + "");
+ x = naturalLogarithm(new Ctor(c0 + "." + c.slice(1)), wpr - guard).plus(t);
+ Ctor.precision = pr;
+ return sd == null ? finalise(x, pr, rm, external = true) : x;
+ }
+ x1 = x;
+ sum = numerator = x = divide(x.minus(1), x.plus(1), wpr, 1);
+ x2 = finalise(x.times(x), wpr, 1);
+ denominator = 3;
+ for (;; ) {
+ numerator = finalise(numerator.times(x2), wpr, 1);
+ t = sum.plus(divide(numerator, new Ctor(denominator), wpr, 1));
+ if (digitsToString(t.d).slice(0, wpr) === digitsToString(sum.d).slice(0, wpr)) {
+ sum = sum.times(2);
+ if (e !== 0)
+ sum = sum.plus(getLn10(Ctor, wpr + 2, pr).times(e + ""));
+ sum = divide(sum, new Ctor(n), wpr, 1);
+ if (sd == null) {
+ if (checkRoundingDigits(sum.d, wpr - guard, rm, rep)) {
+ Ctor.precision = wpr += guard;
+ t = numerator = x = divide(x1.minus(1), x1.plus(1), wpr, 1);
+ x2 = finalise(x.times(x), wpr, 1);
+ denominator = rep = 1;
+ } else {
+ return finalise(sum, Ctor.precision = pr, rm, external = true);
+ }
+ } else {
+ Ctor.precision = pr;
+ return sum;
+ }
+ }
+ sum = t;
+ denominator += 2;
+ }
+}
+function nonFiniteToString(x) {
+ return String(x.s * x.s / 0);
+}
+function parseDecimal(x, str) {
+ var e, i, len;
+ if ((e = str.indexOf(".")) > -1)
+ str = str.replace(".", "");
+ if ((i = str.search(/e/i)) > 0) {
+ if (e < 0)
+ e = i;
+ e += +str.slice(i + 1);
+ str = str.substring(0, i);
+ } else if (e < 0) {
+ e = str.length;
+ }
+ for (i = 0;str.charCodeAt(i) === 48; i++)
+ ;
+ for (len = str.length;str.charCodeAt(len - 1) === 48; --len)
+ ;
+ str = str.slice(i, len);
+ if (str) {
+ len -= i;
+ x.e = e = e - i - 1;
+ x.d = [];
+ i = (e + 1) % LOG_BASE;
+ if (e < 0)
+ i += LOG_BASE;
+ if (i < len) {
+ if (i)
+ x.d.push(+str.slice(0, i));
+ for (len -= LOG_BASE;i < len; )
+ x.d.push(+str.slice(i, i += LOG_BASE));
+ str = str.slice(i);
+ i = LOG_BASE - str.length;
+ } else {
+ i -= len;
+ }
+ for (;i--; )
+ str += "0";
+ x.d.push(+str);
+ if (external) {
+ if (x.e > x.constructor.maxE) {
+ x.d = null;
+ x.e = NaN;
+ } else if (x.e < x.constructor.minE) {
+ x.e = 0;
+ x.d = [0];
+ }
+ }
+ } else {
+ x.e = 0;
+ x.d = [0];
+ }
+ return x;
+}
+function parseOther(x, str) {
+ var base, Ctor, divisor, i, isFloat, len, p, xd, xe;
+ if (str.indexOf("_") > -1) {
+ str = str.replace(/(\d)_(?=\d)/g, "$1");
+ if (isDecimal.test(str))
+ return parseDecimal(x, str);
+ } else if (str === "Infinity" || str === "NaN") {
+ if (!+str)
+ x.s = NaN;
+ x.e = NaN;
+ x.d = null;
+ return x;
+ }
+ if (isHex.test(str)) {
+ base = 16;
+ str = str.toLowerCase();
+ } else if (isBinary.test(str)) {
+ base = 2;
+ } else if (isOctal.test(str)) {
+ base = 8;
+ } else {
+ throw Error(invalidArgument + str);
+ }
+ i = str.search(/p/i);
+ if (i > 0) {
+ p = +str.slice(i + 1);
+ str = str.substring(2, i);
+ } else {
+ str = str.slice(2);
+ }
+ i = str.indexOf(".");
+ isFloat = i >= 0;
+ Ctor = x.constructor;
+ if (isFloat) {
+ str = str.replace(".", "");
+ len = str.length;
+ i = len - i;
+ divisor = intPow(Ctor, new Ctor(base), i, i * 2);
+ }
+ xd = convertBase(str, base, BASE);
+ xe = xd.length - 1;
+ for (i = xe;xd[i] === 0; --i)
+ xd.pop();
+ if (i < 0)
+ return new Ctor(x.s * 0);
+ x.e = getBase10Exponent(xd, xe);
+ x.d = xd;
+ external = false;
+ if (isFloat)
+ x = divide(x, divisor, len * 4);
+ if (p)
+ x = x.times(Math.abs(p) < 54 ? mathpow(2, p) : Decimal.pow(2, p));
+ external = true;
+ return x;
+}
+function sine(Ctor, x) {
+ var k, len = x.d.length;
+ if (len < 3) {
+ return x.isZero() ? x : taylorSeries(Ctor, 2, x, x);
+ }
+ k = 1.4 * Math.sqrt(len);
+ k = k > 16 ? 16 : k | 0;
+ x = x.times(1 / tinyPow(5, k));
+ x = taylorSeries(Ctor, 2, x, x);
+ var sin2_x, d5 = new Ctor(5), d16 = new Ctor(16), d20 = new Ctor(20);
+ for (;k--; ) {
+ sin2_x = x.times(x);
+ x = x.times(d5.plus(sin2_x.times(d16.times(sin2_x).minus(d20))));
+ }
+ return x;
+}
+function taylorSeries(Ctor, n, x, y, isHyperbolic) {
+ var j, t, u, x2, i = 1, pr = Ctor.precision, k = Math.ceil(pr / LOG_BASE);
+ external = false;
+ x2 = x.times(x);
+ u = new Ctor(y);
+ for (;; ) {
+ t = divide(u.times(x2), new Ctor(n++ * n++), pr, 1);
+ u = isHyperbolic ? y.plus(t) : y.minus(t);
+ y = divide(t.times(x2), new Ctor(n++ * n++), pr, 1);
+ t = u.plus(y);
+ if (t.d[k] !== undefined) {
+ for (j = k;t.d[j] === u.d[j] && j--; )
+ ;
+ if (j == -1)
+ break;
+ }
+ j = u;
+ u = y;
+ y = t;
+ t = j;
+ i++;
+ }
+ external = true;
+ t.d.length = k + 1;
+ return t;
+}
+function tinyPow(b, e) {
+ var n = b;
+ while (--e)
+ n *= b;
+ return n;
+}
+function toLessThanHalfPi(Ctor, x) {
+ var t, isNeg = x.s < 0, pi = getPi(Ctor, Ctor.precision, 1), halfPi = pi.times(0.5);
+ x = x.abs();
+ if (x.lte(halfPi)) {
+ quadrant = isNeg ? 4 : 1;
+ return x;
+ }
+ t = x.divToInt(pi);
+ if (t.isZero()) {
+ quadrant = isNeg ? 3 : 2;
+ } else {
+ x = x.minus(t.times(pi));
+ if (x.lte(halfPi)) {
+ quadrant = isOdd(t) ? isNeg ? 2 : 3 : isNeg ? 4 : 1;
+ return x;
+ }
+ quadrant = isOdd(t) ? isNeg ? 1 : 4 : isNeg ? 3 : 2;
+ }
+ return x.minus(pi).abs();
+}
+function toStringBinary(x, baseOut, sd, rm) {
+ var base, e, i, k, len, roundUp, str, xd, y, Ctor = x.constructor, isExp = sd !== undefined;
+ if (isExp) {
+ checkInt32(sd, 1, MAX_DIGITS);
+ if (rm === undefined)
+ rm = Ctor.rounding;
+ else
+ checkInt32(rm, 0, 8);
+ } else {
+ sd = Ctor.precision;
+ rm = Ctor.rounding;
+ }
+ if (!x.isFinite()) {
+ str = nonFiniteToString(x);
+ } else {
+ str = finiteToString(x);
+ i = str.indexOf(".");
+ if (isExp) {
+ base = 2;
+ if (baseOut == 16) {
+ sd = sd * 4 - 3;
+ } else if (baseOut == 8) {
+ sd = sd * 3 - 2;
+ }
+ } else {
+ base = baseOut;
+ }
+ if (i >= 0) {
+ str = str.replace(".", "");
+ y = new Ctor(1);
+ y.e = str.length - i;
+ y.d = convertBase(finiteToString(y), 10, base);
+ y.e = y.d.length;
+ }
+ xd = convertBase(str, 10, base);
+ e = len = xd.length;
+ for (;xd[--len] == 0; )
+ xd.pop();
+ if (!xd[0]) {
+ str = isExp ? "0p+0" : "0";
+ } else {
+ if (i < 0) {
+ e--;
+ } else {
+ x = new Ctor(x);
+ x.d = xd;
+ x.e = e;
+ x = divide(x, y, sd, rm, 0, base);
+ xd = x.d;
+ e = x.e;
+ roundUp = inexact;
+ }
+ i = xd[sd];
+ k = base / 2;
+ roundUp = roundUp || xd[sd + 1] !== undefined;
+ roundUp = rm < 4 ? (i !== undefined || roundUp) && (rm === 0 || rm === (x.s < 0 ? 3 : 2)) : i > k || i === k && (rm === 4 || roundUp || rm === 6 && xd[sd - 1] & 1 || rm === (x.s < 0 ? 8 : 7));
+ xd.length = sd;
+ if (roundUp) {
+ for (;++xd[--sd] > base - 1; ) {
+ xd[sd] = 0;
+ if (!sd) {
+ ++e;
+ xd.unshift(1);
+ }
+ }
+ }
+ for (len = xd.length;!xd[len - 1]; --len)
+ ;
+ for (i = 0, str = "";i < len; i++)
+ str += NUMERALS.charAt(xd[i]);
+ if (isExp) {
+ if (len > 1) {
+ if (baseOut == 16 || baseOut == 8) {
+ i = baseOut == 16 ? 4 : 3;
+ for (--len;len % i; len++)
+ str += "0";
+ xd = convertBase(str, base, baseOut);
+ for (len = xd.length;!xd[len - 1]; --len)
+ ;
+ for (i = 1, str = "1.";i < len; i++)
+ str += NUMERALS.charAt(xd[i]);
+ } else {
+ str = str.charAt(0) + "." + str.slice(1);
+ }
+ }
+ str = str + (e < 0 ? "p" : "p+") + e;
+ } else if (e < 0) {
+ for (;++e; )
+ str = "0" + str;
+ str = "0." + str;
+ } else {
+ if (++e > len)
+ for (e -= len;e--; )
+ str += "0";
+ else if (e < len)
+ str = str.slice(0, e) + "." + str.slice(e);
+ }
+ }
+ str = (baseOut == 16 ? "0x" : baseOut == 2 ? "0b" : baseOut == 8 ? "0o" : "") + str;
+ }
+ return x.s < 0 ? "-" + str : str;
+}
+function truncate(arr, len) {
+ if (arr.length > len) {
+ arr.length = len;
+ return true;
+ }
+}
+function abs(x) {
+ return new this(x).abs();
+}
+function acos(x) {
+ return new this(x).acos();
+}
+function acosh(x) {
+ return new this(x).acosh();
+}
+function add(x, y) {
+ return new this(x).plus(y);
+}
+function asin(x) {
+ return new this(x).asin();
+}
+function asinh(x) {
+ return new this(x).asinh();
+}
+function atan(x) {
+ return new this(x).atan();
+}
+function atanh(x) {
+ return new this(x).atanh();
+}
+function atan2(y, x) {
+ y = new this(y);
+ x = new this(x);
+ var r, pr = this.precision, rm = this.rounding, wpr = pr + 4;
+ if (!y.s || !x.s) {
+ r = new this(NaN);
+ } else if (!y.d && !x.d) {
+ r = getPi(this, wpr, 1).times(x.s > 0 ? 0.25 : 0.75);
+ r.s = y.s;
+ } else if (!x.d || y.isZero()) {
+ r = x.s < 0 ? getPi(this, pr, rm) : new this(0);
+ r.s = y.s;
+ } else if (!y.d || x.isZero()) {
+ r = getPi(this, wpr, 1).times(0.5);
+ r.s = y.s;
+ } else if (x.s < 0) {
+ this.precision = wpr;
+ this.rounding = 1;
+ r = this.atan(divide(y, x, wpr, 1));
+ x = getPi(this, wpr, 1);
+ this.precision = pr;
+ this.rounding = rm;
+ r = y.s < 0 ? r.minus(x) : r.plus(x);
+ } else {
+ r = this.atan(divide(y, x, wpr, 1));
+ }
+ return r;
+}
+function cbrt(x) {
+ return new this(x).cbrt();
+}
+function ceil(x) {
+ return finalise(x = new this(x), x.e + 1, 2);
+}
+function clamp(x, min, max) {
+ return new this(x).clamp(min, max);
+}
+function config(obj) {
+ if (!obj || typeof obj !== "object")
+ throw Error(decimalError + "Object expected");
+ var i, p, v, useDefaults = obj.defaults === true, ps = [
+ "precision",
+ 1,
+ MAX_DIGITS,
+ "rounding",
+ 0,
+ 8,
+ "toExpNeg",
+ -EXP_LIMIT,
+ 0,
+ "toExpPos",
+ 0,
+ EXP_LIMIT,
+ "maxE",
+ 0,
+ EXP_LIMIT,
+ "minE",
+ -EXP_LIMIT,
+ 0,
+ "modulo",
+ 0,
+ 9
+ ];
+ for (i = 0;i < ps.length; i += 3) {
+ if (p = ps[i], useDefaults)
+ this[p] = DEFAULTS[p];
+ if ((v = obj[p]) !== undefined) {
+ if (mathfloor(v) === v && v >= ps[i + 1] && v <= ps[i + 2])
+ this[p] = v;
+ else
+ throw Error(invalidArgument + p + ": " + v);
+ }
+ }
+ if (p = "crypto", useDefaults)
+ this[p] = DEFAULTS[p];
+ if ((v = obj[p]) !== undefined) {
+ if (v === true || v === false || v === 0 || v === 1) {
+ if (v) {
+ if (typeof crypto != "undefined" && crypto && (crypto.getRandomValues || crypto.randomBytes)) {
+ this[p] = true;
+ } else {
+ throw Error(cryptoUnavailable);
+ }
+ } else {
+ this[p] = false;
+ }
+ } else {
+ throw Error(invalidArgument + p + ": " + v);
+ }
+ }
+ return this;
+}
+function cos(x) {
+ return new this(x).cos();
+}
+function cosh(x) {
+ return new this(x).cosh();
+}
+function clone(obj) {
+ var i, p, ps;
+ function Decimal(v) {
+ var e, i2, t, x = this;
+ if (!(x instanceof Decimal))
+ return new Decimal(v);
+ x.constructor = Decimal;
+ if (isDecimalInstance(v)) {
+ x.s = v.s;
+ if (external) {
+ if (!v.d || v.e > Decimal.maxE) {
+ x.e = NaN;
+ x.d = null;
+ } else if (v.e < Decimal.minE) {
+ x.e = 0;
+ x.d = [0];
+ } else {
+ x.e = v.e;
+ x.d = v.d.slice();
+ }
+ } else {
+ x.e = v.e;
+ x.d = v.d ? v.d.slice() : v.d;
+ }
+ return;
+ }
+ t = typeof v;
+ if (t === "number") {
+ if (v === 0) {
+ x.s = 1 / v < 0 ? -1 : 1;
+ x.e = 0;
+ x.d = [0];
+ return;
+ }
+ if (v < 0) {
+ v = -v;
+ x.s = -1;
+ } else {
+ x.s = 1;
+ }
+ if (v === ~~v && v < 1e7) {
+ for (e = 0, i2 = v;i2 >= 10; i2 /= 10)
+ e++;
+ if (external) {
+ if (e > Decimal.maxE) {
+ x.e = NaN;
+ x.d = null;
+ } else if (e < Decimal.minE) {
+ x.e = 0;
+ x.d = [0];
+ } else {
+ x.e = e;
+ x.d = [v];
+ }
+ } else {
+ x.e = e;
+ x.d = [v];
+ }
+ return;
+ }
+ if (v * 0 !== 0) {
+ if (!v)
+ x.s = NaN;
+ x.e = NaN;
+ x.d = null;
+ return;
+ }
+ return parseDecimal(x, v.toString());
+ }
+ if (t === "string") {
+ if ((i2 = v.charCodeAt(0)) === 45) {
+ v = v.slice(1);
+ x.s = -1;
+ } else {
+ if (i2 === 43)
+ v = v.slice(1);
+ x.s = 1;
+ }
+ return isDecimal.test(v) ? parseDecimal(x, v) : parseOther(x, v);
+ }
+ if (t === "bigint") {
+ if (v < 0) {
+ v = -v;
+ x.s = -1;
+ } else {
+ x.s = 1;
+ }
+ return parseDecimal(x, v.toString());
+ }
+ throw Error(invalidArgument + v);
+ }
+ Decimal.prototype = P;
+ Decimal.ROUND_UP = 0;
+ Decimal.ROUND_DOWN = 1;
+ Decimal.ROUND_CEIL = 2;
+ Decimal.ROUND_FLOOR = 3;
+ Decimal.ROUND_HALF_UP = 4;
+ Decimal.ROUND_HALF_DOWN = 5;
+ Decimal.ROUND_HALF_EVEN = 6;
+ Decimal.ROUND_HALF_CEIL = 7;
+ Decimal.ROUND_HALF_FLOOR = 8;
+ Decimal.EUCLID = 9;
+ Decimal.config = Decimal.set = config;
+ Decimal.clone = clone;
+ Decimal.isDecimal = isDecimalInstance;
+ Decimal.abs = abs;
+ Decimal.acos = acos;
+ Decimal.acosh = acosh;
+ Decimal.add = add;
+ Decimal.asin = asin;
+ Decimal.asinh = asinh;
+ Decimal.atan = atan;
+ Decimal.atanh = atanh;
+ Decimal.atan2 = atan2;
+ Decimal.cbrt = cbrt;
+ Decimal.ceil = ceil;
+ Decimal.clamp = clamp;
+ Decimal.cos = cos;
+ Decimal.cosh = cosh;
+ Decimal.div = div;
+ Decimal.exp = exp;
+ Decimal.floor = floor;
+ Decimal.hypot = hypot;
+ Decimal.ln = ln;
+ Decimal.log = log;
+ Decimal.log10 = log10;
+ Decimal.log2 = log2;
+ Decimal.max = max;
+ Decimal.min = min;
+ Decimal.mod = mod;
+ Decimal.mul = mul;
+ Decimal.pow = pow;
+ Decimal.random = random;
+ Decimal.round = round;
+ Decimal.sign = sign;
+ Decimal.sin = sin;
+ Decimal.sinh = sinh;
+ Decimal.sqrt = sqrt;
+ Decimal.sub = sub;
+ Decimal.sum = sum;
+ Decimal.tan = tan;
+ Decimal.tanh = tanh;
+ Decimal.trunc = trunc;
+ if (obj === undefined)
+ obj = {};
+ if (obj) {
+ if (obj.defaults !== true) {
+ ps = ["precision", "rounding", "toExpNeg", "toExpPos", "maxE", "minE", "modulo", "crypto"];
+ for (i = 0;i < ps.length; )
+ if (!obj.hasOwnProperty(p = ps[i++]))
+ obj[p] = this[p];
+ }
+ }
+ Decimal.config(obj);
+ return Decimal;
+}
+function div(x, y) {
+ return new this(x).div(y);
+}
+function exp(x) {
+ return new this(x).exp();
+}
+function floor(x) {
+ return finalise(x = new this(x), x.e + 1, 3);
+}
+function hypot() {
+ var i, n, t = new this(0);
+ external = false;
+ for (i = 0;i < arguments.length; ) {
+ n = new this(arguments[i++]);
+ if (!n.d) {
+ if (n.s) {
+ external = true;
+ return new this(1 / 0);
+ }
+ t = n;
+ } else if (t.d) {
+ t = t.plus(n.times(n));
+ }
+ }
+ external = true;
+ return t.sqrt();
+}
+function isDecimalInstance(obj) {
+ return obj instanceof Decimal || obj && obj.toStringTag === tag || false;
+}
+function ln(x) {
+ return new this(x).ln();
+}
+function log(x, y) {
+ return new this(x).log(y);
+}
+function log2(x) {
+ return new this(x).log(2);
+}
+function log10(x) {
+ return new this(x).log(10);
+}
+function max() {
+ return maxOrMin(this, arguments, -1);
+}
+function min() {
+ return maxOrMin(this, arguments, 1);
+}
+function mod(x, y) {
+ return new this(x).mod(y);
+}
+function mul(x, y) {
+ return new this(x).mul(y);
+}
+function pow(x, y) {
+ return new this(x).pow(y);
+}
+function random(sd) {
+ var d, e, k, n, i = 0, r = new this(1), rd = [];
+ if (sd === undefined)
+ sd = this.precision;
+ else
+ checkInt32(sd, 1, MAX_DIGITS);
+ k = Math.ceil(sd / LOG_BASE);
+ if (!this.crypto) {
+ for (;i < k; )
+ rd[i++] = Math.random() * 1e7 | 0;
+ } else if (crypto.getRandomValues) {
+ d = crypto.getRandomValues(new Uint32Array(k));
+ for (;i < k; ) {
+ n = d[i];
+ if (n >= 4290000000) {
+ d[i] = crypto.getRandomValues(new Uint32Array(1))[0];
+ } else {
+ rd[i++] = n % 1e7;
+ }
+ }
+ } else if (crypto.randomBytes) {
+ d = crypto.randomBytes(k *= 4);
+ for (;i < k; ) {
+ n = d[i] + (d[i + 1] << 8) + (d[i + 2] << 16) + ((d[i + 3] & 127) << 24);
+ if (n >= 2140000000) {
+ crypto.randomBytes(4).copy(d, i);
+ } else {
+ rd.push(n % 1e7);
+ i += 4;
+ }
+ }
+ i = k / 4;
+ } else {
+ throw Error(cryptoUnavailable);
+ }
+ k = rd[--i];
+ sd %= LOG_BASE;
+ if (k && sd) {
+ n = mathpow(10, LOG_BASE - sd);
+ rd[i] = (k / n | 0) * n;
+ }
+ for (;rd[i] === 0; i--)
+ rd.pop();
+ if (i < 0) {
+ e = 0;
+ rd = [0];
+ } else {
+ e = -1;
+ for (;rd[0] === 0; e -= LOG_BASE)
+ rd.shift();
+ for (k = 1, n = rd[0];n >= 10; n /= 10)
+ k++;
+ if (k < LOG_BASE)
+ e -= LOG_BASE - k;
+ }
+ r.e = e;
+ r.d = rd;
+ return r;
+}
+function round(x) {
+ return finalise(x = new this(x), x.e + 1, this.rounding);
+}
+function sign(x) {
+ x = new this(x);
+ return x.d ? x.d[0] ? x.s : 0 * x.s : x.s || NaN;
+}
+function sin(x) {
+ return new this(x).sin();
+}
+function sinh(x) {
+ return new this(x).sinh();
+}
+function sqrt(x) {
+ return new this(x).sqrt();
+}
+function sub(x, y) {
+ return new this(x).sub(y);
+}
+function sum() {
+ var i = 0, args = arguments, x = new this(args[i]);
+ external = false;
+ for (;x.s && ++i < args.length; )
+ x = x.plus(args[i]);
+ external = true;
+ return finalise(x, this.precision, this.rounding);
+}
+function tan(x) {
+ return new this(x).tan();
+}
+function tanh(x) {
+ return new this(x).tanh();
+}
+function trunc(x) {
+ return finalise(x = new this(x), x.e + 1, 1);
+}
+P[Symbol.for("nodejs.util.inspect.custom")] = P.toString;
+P[Symbol.toStringTag] = "Decimal";
+var Decimal = P.constructor = clone(DEFAULTS);
+LN10 = new Decimal(LN10);
+PI = new Decimal(PI);
+var decimal_default = Decimal;
+
+// src/colour/hex-to-hsl.ts
+var hexToHsl = (hex) => {
+ const red = new decimal_default(parseInt(hex.slice(1, 3), 16)).div(255);
+ const green = new decimal_default(parseInt(hex.slice(3, 5), 16)).div(255);
+ const blue = new decimal_default(parseInt(hex.slice(5, 7), 16)).div(255);
+ const cmin = decimal_default.min(red, green, blue);
+ const cmax = decimal_default.max(red, green, blue);
+ const delta = cmax.minus(cmin);
+ let h = new decimal_default(0);
+ let s = new decimal_default(0);
+ let l = cmax.plus(cmin).div(2);
+ if (!delta.isZero()) {
+ if (cmax.equals(red)) {
+ h = green.minus(blue).div(delta).mod(6);
+ } else if (cmax.equals(green)) {
+ h = blue.minus(red).div(delta).plus(2);
+ } else {
+ h = red.minus(green).div(delta).plus(4);
+ }
+ h = h.times(60);
+ if (h.isNegative())
+ h = h.plus(360);
+ }
+ if (!delta.isZero()) {
+ s = delta.div(new decimal_default(1).minus(l.times(2).minus(1).abs()));
+ }
+ const hFinal = h.toDecimalPlaces(0, decimal_default.ROUND_HALF_UP).toNumber();
+ const sFinal = s.times(100).toDecimalPlaces(0, decimal_default.ROUND_HALF_UP).toNumber();
+ const lFinal = l.times(100).toDecimalPlaces(0, decimal_default.ROUND_HALF_UP).toNumber();
+ return `hsl(${hFinal}, ${sFinal}%, ${lFinal}%)`;
+};
+// src/numbers/scientific-notation.ts
+var toScientificNotation = (num, precision = 0) => {
+ const exponential = precision > 0 ? Number(num).toExponential(precision) : Number(num).toExponential(0);
+ const match = exponential.match(/^(-?\d+(?:\.\d+)?)e([+-]?\d+)$/);
+ if (!match) {
+ return num.toString();
+ }
+ let coefficient = match[1];
+ const exponent = parseInt(match[2], 10).toString();
+ if (precision === 0) {
+ coefficient = Math.round(Number(coefficient)).toString();
+ }
+ return `${coefficient} × 10${exponent} `;
+};
+// src/units/temperatre-converter.ts
+class TemperatureConverter {
+ static CELSIUS_TO_FAHRENHEIT = 1.8;
+ static FAHRENHEIT_TO_CELSIUS = 0.5555555555555556;
+ static celsiusToFahrenheit(celsius) {
+ return celsius * this.CELSIUS_TO_FAHRENHEIT + 32;
+ }
+ static fahrenheitToCelsius(fahrenheit) {
+ return (fahrenheit - 32) * this.FAHRENHEIT_TO_CELSIUS;
+ }
+ static celsiusFanOvenToFahrenheit(celsiusFanOven) {
+ const celsius = new Decimal(celsiusFanOven).div(0.9);
+ return this.celsiusToFahrenheit(celsius.toNumber());
+ }
+ static fahrenheitToCelsiusFanOven(fahrenheit) {
+ const celsius = new Decimal(this.fahrenheitToCelsius(fahrenheit));
+ return celsius.mul(0.9).toNumber();
+ }
+ static celsiusToCelsiusFanOven(celsius) {
+ const c = new Decimal(celsius);
+ return c.mul(0.9).toNumber();
+ }
+ static celsiusFanOvenToCelsius(celsiusFanOven) {
+ const c = new Decimal(celsiusFanOven);
+ return c.div(0.9).toNumber();
+ }
+}
+
+// src/units/mass-converter.ts
+class MassConverter {
+ static GRAMS_TO_OUNCES = new Decimal(0.035274);
+ static KILOGRAMS_TO_POUNDS = new Decimal(2.20462);
+ static KILOGRAMS_TO_OUNCES = new Decimal(35.274);
+ static kilogramsToPounds(kilos) {
+ const result = new Decimal(kilos).mul(this.KILOGRAMS_TO_POUNDS);
+ return Number(result.toFixed(5));
+ }
+ static poundsToKilograms(pounds) {
+ const result = new Decimal(pounds).div(this.KILOGRAMS_TO_POUNDS);
+ return Number(result.toFixed(5));
+ }
+ static gramsToKilograms(grams) {
+ const result = new Decimal(grams).div(1000);
+ return Number(result.toFixed(5));
+ }
+ static kilogramsToGrams(kilos) {
+ const result = new Decimal(kilos).mul(1000);
+ return Number(result.toFixed(5));
+ }
+ static gramsToOunces(grams) {
+ const result = new Decimal(grams).mul(this.GRAMS_TO_OUNCES);
+ return Number(result.toFixed(5));
+ }
+ static ouncesToGrams(ounces) {
+ const result = new Decimal(ounces).div(this.GRAMS_TO_OUNCES);
+ return Number(result.toFixed(5));
+ }
+ static ouncesToPounds(ounces) {
+ const result = new Decimal(ounces).div(16);
+ return Number(result.toFixed(5));
+ }
+ static poundsToOunces(pounds) {
+ const result = new Decimal(pounds).mul(16);
+ return Number(result.toFixed(5));
+ }
+ static gramsToPounds(grams) {
+ const result = new Decimal(grams).mul(this.KILOGRAMS_TO_POUNDS).div(1000);
+ return Number(result.toFixed(5));
+ }
+ static poundsToGrams(pounds) {
+ const result = new Decimal(pounds).div(this.KILOGRAMS_TO_POUNDS).mul(1000);
+ return Number(result.toFixed(5));
+ }
+ static kilogramsToOunces(kilos) {
+ const result = new Decimal(kilos).mul(this.KILOGRAMS_TO_OUNCES);
+ return Number(result.toFixed(5));
+ }
+ static ouncesToKilograms(ounces) {
+ const result = new Decimal(ounces).div(this.KILOGRAMS_TO_OUNCES);
+ return Number(result.toFixed(5));
+ }
+}
+
+// src/units/volume-converter.ts
+class VolumeConverter {
+ static LITRES_TO_PINTS = new Decimal(1.75975);
+ static PINTS_TO_LITRES = new Decimal(0.568261);
+ static LITRES_TO_FLUID_OUNCES = new Decimal(33.814);
+ static FLUID_OUNCES_TO_LITRES = new Decimal(0.0295735);
+ static GALLONS_TO_PINTS = new Decimal(8);
+ static PINTS_TO_GALLONS = new Decimal(0.125);
+ static GALLONS_TO_MILLILITRES = new Decimal(3785.41);
+ static MILLILITRES_TO_GALLONS = new Decimal(0.000264172);
+ static PINTS_TO_FLUID_OUNCES = new Decimal(20);
+ static FLUID_OUNCES_TO_PINTS = new Decimal(0.05);
+ static MILLILITRE_TO_FLUID_OUNCE = new Decimal(0.033814);
+ static LITRE_TO_GALLON = new Decimal(0.264172);
+ static MILLILITRE_TO_PINT = new Decimal(0.00176);
+ static litresToPints(litres) {
+ const result = new Decimal(litres).mul(this.LITRES_TO_PINTS);
+ return Number(result.toFixed(5));
+ }
+ static pintsToLitres(pints) {
+ const result = new Decimal(pints).mul(this.PINTS_TO_LITRES);
+ return Number(result.toFixed(5));
+ }
+ static litresToFluidOunces(litres) {
+ const result = new Decimal(litres).mul(this.LITRES_TO_FLUID_OUNCES);
+ return Number(result.toFixed(5));
+ }
+ static fluidOuncesToLitres(fluidOunces) {
+ const result = new Decimal(fluidOunces).mul(this.FLUID_OUNCES_TO_LITRES);
+ return Number(result.toFixed(5));
+ }
+ static gallonsToPints(gallons) {
+ const result = new Decimal(gallons).mul(this.GALLONS_TO_PINTS);
+ return Number(result.toFixed(5));
+ }
+ static pintsToGallons(pints) {
+ const result = new Decimal(pints).mul(this.PINTS_TO_GALLONS);
+ return Number(result.toFixed(5));
+ }
+ static gallonsToMillilitres(gallons) {
+ const result = new Decimal(gallons).mul(this.GALLONS_TO_MILLILITRES);
+ return Number(result.toFixed(5));
+ }
+ static millilitresToGallons(millilitres) {
+ const result = new Decimal(millilitres).mul(this.MILLILITRES_TO_GALLONS);
+ return Number(result.toFixed(5));
+ }
+ static pintsToFluidOunces(pints) {
+ const result = new Decimal(pints).mul(this.PINTS_TO_FLUID_OUNCES);
+ return Number(result.toFixed(5));
+ }
+ static fluidOuncesToPints(fluidOunces) {
+ const result = new Decimal(fluidOunces).mul(this.FLUID_OUNCES_TO_PINTS);
+ return Number(result.toFixed(5));
+ }
+ static millilitresToFluidOunces(millilitres) {
+ const result = new Decimal(millilitres).mul(this.MILLILITRE_TO_FLUID_OUNCE);
+ return Number(result.toFixed(5));
+ }
+ static fluidOuncesToMillilitres(fluidOunces) {
+ const result = new Decimal(fluidOunces).div(this.MILLILITRE_TO_FLUID_OUNCE);
+ return Number(result.toFixed(5));
+ }
+ static litresToGallons(litres) {
+ const result = new Decimal(litres).mul(this.LITRE_TO_GALLON);
+ return Number(result.toFixed(5));
+ }
+ static litresToMillilitres(litres) {
+ const result = new Decimal(litres).mul(1000);
+ return Number(result.toFixed(5));
+ }
+ static millilitresToLitres(millilitres) {
+ const result = new Decimal(millilitres).div(1000);
+ return Number(result.toFixed(5));
+ }
+ static gallonsToLitres(gallons) {
+ const result = new Decimal(gallons).div(this.LITRE_TO_GALLON);
+ return Number(result.toFixed(5));
+ }
+ static millilitresToPints(millilitres) {
+ const result = new Decimal(millilitres).mul(this.MILLILITRE_TO_PINT);
+ return Number(result.toFixed(5));
+ }
+ static pintsToMillilitres(pints) {
+ const result = new Decimal(pints).div(this.MILLILITRE_TO_PINT);
+ return Number(result.toFixed(5));
+ }
+ static gallonsToFluidOunces(gallons) {
+ const litres = this.gallonsToLitres(gallons);
+ const millilitres = this.litresToMillilitres(litres);
+ return this.millilitresToFluidOunces(millilitres);
+ }
+ static fluidOuncesToGallons(fluidOunces) {
+ const millilitres = this.fluidOuncesToMillilitres(fluidOunces);
+ const litres = this.millilitresToLitres(millilitres);
+ return this.litresToGallons(litres);
+ }
+}
+
+// src/units/model.ts
+var MASS_UNITS = {
+ GRAM: { long: "gram", short: "g" },
+ KILOGRAM: { long: "kilogram", short: "kg" },
+ OUNCE: { long: "ounce", short: "oz" },
+ POUND: { long: "pound", short: "lb" }
+};
+var TEMPERATURE_UNITS = {
+ CELSIUS: { long: "celsius", short: "c" },
+ CELSIUS_FAN_OVEN: { long: "celsius (fan oven)", short: "c (fan)" },
+ FAHRENHEIT: { long: "fahrenheit", short: "f" }
+};
+var VOLUME_UNITS = {
+ MILLILITRE: { long: "millilitre", short: "ml" },
+ FLUID_OUNCE: { long: "fluid ounce", short: "fl oz" },
+ LITRE: { long: "litre", short: "l" },
+ GALLON: { long: "gallon", short: "gal" },
+ PINT: { long: "pint", short: "pt" }
+};
+var DISTANCE_UNITS = {
+ CENTIMETRE: { long: "centimetre", short: "cm" },
+ METRE: { long: "metre", short: "m" },
+ KILOMETRE: { long: "kilometre", short: "km" },
+ INCH: { long: "inch", short: "in" },
+ FOOT: { long: "foot", short: "ft" },
+ YARD: { long: "yard", short: "yd" },
+ MILE: { long: "mile", short: "mi" }
+};
+function isMassUnit(unit) {
+ return Object.values(MASS_UNITS).some((u) => u.short === unit);
+}
+function isTemperatureUnit(unit) {
+ return Object.values(TEMPERATURE_UNITS).some((u) => u.short === unit);
+}
+function isVolumeUnit(unit) {
+ return Object.values(VOLUME_UNITS).some((u) => u.short === unit);
+}
+function isDistanceUnit(unit) {
+ return Object.values(DISTANCE_UNITS).some((u) => u.short === unit);
+}
+var unitMap = {
+ ...MASS_UNITS,
+ ...TEMPERATURE_UNITS,
+ ...VOLUME_UNITS,
+ ...DISTANCE_UNITS
+};
+
+// src/units/distance-converter.ts
+class DistanceConverter {
+ static METRES_TO_YARDS = new Decimal(1.09361);
+ static KILOMETRES_TO_MILES = new Decimal(0.621371);
+ static CENTIMETRES_TO_INCHES = new Decimal(0.393701);
+ static METRES_TO_FEET = new Decimal(3.28084);
+ static CENTIMETRES_TO_FEET = new Decimal(0.0328084);
+ static FEET_TO_INCHES = new Decimal(12);
+ static metresToYards(metres) {
+ const result = new Decimal(metres).mul(this.METRES_TO_YARDS);
+ return Number(result.toFixed(5));
+ }
+ static metresToFeet(metres) {
+ const result = new Decimal(metres).mul(this.METRES_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+ static feetToMetres(feet) {
+ const result = new Decimal(feet).div(this.METRES_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+ static yardsToMetres(yards) {
+ const result = new Decimal(yards).div(this.METRES_TO_YARDS);
+ return Number(result.toFixed(5));
+ }
+ static kilometresToMiles(kilometres) {
+ const result = new Decimal(kilometres).mul(this.KILOMETRES_TO_MILES);
+ return Number(result.toFixed(5));
+ }
+ static milesToKilometres(miles) {
+ const result = new Decimal(miles).div(this.KILOMETRES_TO_MILES);
+ return Number(result.toFixed(5));
+ }
+ static centimetresToInches(centimetres) {
+ const result = new Decimal(centimetres).mul(this.CENTIMETRES_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+ static centimetresToFeet(centimetres) {
+ const result = new Decimal(centimetres).mul(this.CENTIMETRES_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+ static feetToCentimetres(feet) {
+ const result = new Decimal(feet).div(this.CENTIMETRES_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+ static inchesToCentimetres(inches) {
+ const result = new Decimal(inches).div(this.CENTIMETRES_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+ static feetToInches(feet) {
+ const result = new Decimal(feet).mul(this.FEET_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+ static inchesToFeet(inches) {
+ const result = new Decimal(inches).div(this.FEET_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+ static centimetresToMetres(centimetres) {
+ const result = new Decimal(centimetres).div(100);
+ return Number(result.toFixed(5));
+ }
+ static metresToCentimetres(metres) {
+ const result = new Decimal(metres).mul(100);
+ return Number(result.toFixed(5));
+ }
+ static kilometresToMetres(kilometres) {
+ const result = new Decimal(kilometres).mul(1000);
+ return Number(result.toFixed(5));
+ }
+ static metresToKilometres(metres) {
+ const result = new Decimal(metres).div(1000);
+ return Number(result.toFixed(5));
+ }
+ static kilometresToCentimetres(kilometres) {
+ const result = new Decimal(kilometres).mul(1e5);
+ return Number(result.toFixed(5));
+ }
+ static centimetresToKilometres(centimetres) {
+ const result = new Decimal(centimetres).div(1e5);
+ return Number(result.toFixed(5));
+ }
+ static metresToInches(metres) {
+ const result = new Decimal(metres).mul(39.3701);
+ return Number(result.toFixed(5));
+ }
+ static inchesToMetres(inches) {
+ const result = new Decimal(inches).mul(0.0254);
+ return Number(result.toFixed(5));
+ }
+ static kilometresToYards(kilometres) {
+ const result = new Decimal(kilometres).mul(1093.61);
+ return Number(result.toFixed(5));
+ }
+ static yardsToKilometres(yards) {
+ const result = new Decimal(yards).mul(0.0009144);
+ return Number(result.toFixed(5));
+ }
+ static milesToYards(miles) {
+ const result = new Decimal(miles).mul(1760);
+ return Number(result.toFixed(5));
+ }
+ static yardsToMiles(yards) {
+ const result = new Decimal(yards).mul(0.000568182);
+ return Number(result.toFixed(5));
+ }
+ static milesToFeet(miles) {
+ const result = new Decimal(miles).mul(5280);
+ return Number(result.toFixed(5));
+ }
+ static feetToMiles(feet) {
+ const result = new Decimal(feet).mul(0.000189394);
+ return Number(result.toFixed(5));
+ }
+ static milesToInches(miles) {
+ const result = new Decimal(miles).mul(63360);
+ return Number(result.toFixed(5));
+ }
+ static inchesToMiles(inches) {
+ const result = new Decimal(inches).mul(0.0000157828);
+ return Number(result.toFixed(8));
+ }
+ static yardsToFeet(yards) {
+ const result = new Decimal(yards).mul(3);
+ return Number(result.toFixed(5));
+ }
+ static feetToYards(feet) {
+ const result = new Decimal(feet).div(3);
+ return Number(result.toFixed(5));
+ }
+}
+
+// src/units/convert.ts
+var conversionErrorString = (fromUnit, toUnit) => {
+ return `Cannot convert from ${fromUnit} to ${toUnit}`;
+};
+var convertMass = (value, fromUnit, toUnit) => {
+ if (!isMassUnit(toUnit) || !isMassUnit(fromUnit)) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+ if (fromUnit === MASS_UNITS.KILOGRAM.short && toUnit === MASS_UNITS.POUND.short) {
+ return MassConverter.kilogramsToPounds(value);
+ } else if (fromUnit === MASS_UNITS.POUND.short && toUnit === MASS_UNITS.KILOGRAM.short) {
+ return MassConverter.poundsToKilograms(value);
+ } else if (fromUnit === MASS_UNITS.GRAM.short && toUnit === MASS_UNITS.KILOGRAM.short) {
+ return MassConverter.gramsToKilograms(value);
+ } else if (fromUnit === MASS_UNITS.KILOGRAM.short && toUnit === MASS_UNITS.GRAM.short) {
+ return MassConverter.kilogramsToGrams(value);
+ } else if (fromUnit === MASS_UNITS.GRAM.short && toUnit === MASS_UNITS.OUNCE.short) {
+ return MassConverter.gramsToOunces(value);
+ } else if (fromUnit === MASS_UNITS.OUNCE.short && toUnit === MASS_UNITS.GRAM.short) {
+ return MassConverter.ouncesToGrams(value);
+ } else if (fromUnit === MASS_UNITS.OUNCE.short && toUnit === MASS_UNITS.POUND.short) {
+ return MassConverter.ouncesToPounds(value);
+ } else if (fromUnit === MASS_UNITS.POUND.short && toUnit === MASS_UNITS.OUNCE.short) {
+ return MassConverter.poundsToOunces(value);
+ } else if (fromUnit === MASS_UNITS.GRAM.short && toUnit === MASS_UNITS.POUND.short) {
+ return MassConverter.gramsToPounds(value);
+ } else if (fromUnit === MASS_UNITS.POUND.short && toUnit === MASS_UNITS.GRAM.short) {
+ return MassConverter.poundsToGrams(value);
+ } else if (fromUnit === MASS_UNITS.KILOGRAM.short && toUnit === MASS_UNITS.OUNCE.short) {
+ return MassConverter.kilogramsToOunces(value);
+ } else if (fromUnit === MASS_UNITS.OUNCE.short && toUnit === MASS_UNITS.KILOGRAM.short) {
+ return MassConverter.ouncesToKilograms(value);
+ }
+ return conversionErrorString(fromUnit, toUnit);
+};
+var convertTemperature = (value, fromUnit, toUnit) => {
+ if (!isTemperatureUnit(toUnit) || !isTemperatureUnit(fromUnit)) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+ if (fromUnit === TEMPERATURE_UNITS.CELSIUS.short && toUnit === TEMPERATURE_UNITS.FAHRENHEIT.short) {
+ return TemperatureConverter.celsiusToFahrenheit(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.FAHRENHEIT.short && toUnit === TEMPERATURE_UNITS.CELSIUS.short) {
+ return TemperatureConverter.fahrenheitToCelsius(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.CELSIUS_FAN_OVEN.short && toUnit === TEMPERATURE_UNITS.FAHRENHEIT.short) {
+ return TemperatureConverter.celsiusFanOvenToFahrenheit(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.FAHRENHEIT.short && toUnit === TEMPERATURE_UNITS.CELSIUS_FAN_OVEN.short) {
+ return TemperatureConverter.fahrenheitToCelsiusFanOven(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.CELSIUS.short && toUnit === TEMPERATURE_UNITS.CELSIUS_FAN_OVEN.short) {
+ return TemperatureConverter.celsiusToCelsiusFanOven(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.CELSIUS_FAN_OVEN.short && toUnit === TEMPERATURE_UNITS.CELSIUS.short) {
+ return TemperatureConverter.celsiusFanOvenToCelsius(value);
+ }
+ return conversionErrorString(fromUnit, toUnit);
+};
+var convertVolume = (value, fromUnit, toUnit) => {
+ if (!isVolumeUnit(toUnit) || !isVolumeUnit(fromUnit)) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+ if (fromUnit === VOLUME_UNITS.LITRE.short && toUnit === VOLUME_UNITS.PINT.short) {
+ return VolumeConverter.litresToPints(value);
+ } else if (fromUnit === VOLUME_UNITS.PINT.short && toUnit === VOLUME_UNITS.LITRE.short) {
+ return VolumeConverter.pintsToLitres(value);
+ } else if (fromUnit === VOLUME_UNITS.LITRE.short && toUnit === VOLUME_UNITS.FLUID_OUNCE.short) {
+ return VolumeConverter.litresToFluidOunces(value);
+ } else if (fromUnit === VOLUME_UNITS.FLUID_OUNCE.short && toUnit === VOLUME_UNITS.LITRE.short) {
+ return VolumeConverter.fluidOuncesToLitres(value);
+ } else if (fromUnit === VOLUME_UNITS.GALLON.short && toUnit === VOLUME_UNITS.PINT.short) {
+ return VolumeConverter.gallonsToPints(value);
+ } else if (fromUnit === VOLUME_UNITS.PINT.short && toUnit === VOLUME_UNITS.GALLON.short) {
+ return VolumeConverter.pintsToGallons(value);
+ } else if (fromUnit === VOLUME_UNITS.GALLON.short && toUnit === VOLUME_UNITS.MILLILITRE.short) {
+ return VolumeConverter.gallonsToMillilitres(value);
+ } else if (fromUnit === VOLUME_UNITS.MILLILITRE.short && toUnit === VOLUME_UNITS.GALLON.short) {
+ return VolumeConverter.millilitresToGallons(value);
+ } else if (fromUnit === VOLUME_UNITS.PINT.short && toUnit === VOLUME_UNITS.FLUID_OUNCE.short) {
+ return VolumeConverter.pintsToFluidOunces(value);
+ } else if (fromUnit === VOLUME_UNITS.FLUID_OUNCE.short && toUnit === VOLUME_UNITS.PINT.short) {
+ return VolumeConverter.fluidOuncesToPints(value);
+ } else if (fromUnit === VOLUME_UNITS.MILLILITRE.short && toUnit === VOLUME_UNITS.FLUID_OUNCE.short) {
+ return VolumeConverter.millilitresToFluidOunces(value);
+ } else if (fromUnit === VOLUME_UNITS.FLUID_OUNCE.short && toUnit === VOLUME_UNITS.MILLILITRE.short) {
+ return VolumeConverter.fluidOuncesToMillilitres(value);
+ } else if (fromUnit === VOLUME_UNITS.LITRE.short && toUnit === VOLUME_UNITS.GALLON.short) {
+ return VolumeConverter.litresToGallons(value);
+ } else if (fromUnit === VOLUME_UNITS.LITRE.short && toUnit === VOLUME_UNITS.MILLILITRE.short) {
+ return VolumeConverter.litresToMillilitres(value);
+ } else if (fromUnit === VOLUME_UNITS.MILLILITRE.short && toUnit === VOLUME_UNITS.LITRE.short) {
+ return VolumeConverter.millilitresToLitres(value);
+ } else if (fromUnit === VOLUME_UNITS.GALLON.short && toUnit === VOLUME_UNITS.LITRE.short) {
+ return VolumeConverter.gallonsToLitres(value);
+ } else if (fromUnit === VOLUME_UNITS.MILLILITRE.short && toUnit === VOLUME_UNITS.PINT.short) {
+ return VolumeConverter.millilitresToPints(value);
+ } else if (fromUnit === VOLUME_UNITS.PINT.short && toUnit === VOLUME_UNITS.MILLILITRE.short) {
+ return VolumeConverter.pintsToMillilitres(value);
+ } else if (fromUnit === VOLUME_UNITS.GALLON.short && toUnit === VOLUME_UNITS.FLUID_OUNCE.short) {
+ return VolumeConverter.gallonsToFluidOunces(value);
+ } else if (fromUnit === VOLUME_UNITS.FLUID_OUNCE.short && toUnit === VOLUME_UNITS.GALLON.short) {
+ return VolumeConverter.fluidOuncesToGallons(value);
+ }
+ return conversionErrorString(fromUnit, toUnit);
+};
+var convertDistance = (value, fromUnit, toUnit) => {
+ if (!isDistanceUnit(toUnit) || !isDistanceUnit(fromUnit)) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+ if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.YARD.short) {
+ return DistanceConverter.metresToYards(value);
+ } else if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.FOOT.short) {
+ return DistanceConverter.metresToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.feetToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.YARD.short && toUnit === DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.yardsToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.KILOMETRE.short && toUnit === DISTANCE_UNITS.MILE.short) {
+ return DistanceConverter.kilometresToMiles(value);
+ } else if (fromUnit === DISTANCE_UNITS.MILE.short && toUnit === DISTANCE_UNITS.KILOMETRE.short) {
+ return DistanceConverter.milesToKilometres(value);
+ } else if (fromUnit === DISTANCE_UNITS.CENTIMETRE.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.centimetresToInches(value);
+ } else if (fromUnit === DISTANCE_UNITS.CENTIMETRE.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.centimetresToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.CENTIMETRE.short) {
+ return DistanceConverter.feetToCentimetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.INCH.short && toUnit === DISTANCE_UNITS.CENTIMETRE.short) {
+ return DistanceConverter.inchesToCentimetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.feetToInches(value);
+ } else if (fromUnit === DISTANCE_UNITS.INCH.short && toUnit === DISTANCE_UNITS.FOOT.short) {
+ return DistanceConverter.inchesToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.CENTIMETRE.short && toUnit == DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.centimetresToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.CENTIMETRE.short) {
+ return DistanceConverter.metresToCentimetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.KILOMETRE.short && toUnit === DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.kilometresToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.KILOMETRE.short) {
+ return DistanceConverter.metresToKilometres(value);
+ } else if (fromUnit === DISTANCE_UNITS.KILOMETRE.short && toUnit === DISTANCE_UNITS.CENTIMETRE.short) {
+ return DistanceConverter.kilometresToCentimetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.CENTIMETRE.short && toUnit === DISTANCE_UNITS.KILOMETRE.short) {
+ return DistanceConverter.centimetresToKilometres(value);
+ } else if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.metresToInches(value);
+ } else if (fromUnit === DISTANCE_UNITS.INCH.short && toUnit === DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.inchesToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.KILOMETRE.short && toUnit === DISTANCE_UNITS.YARD.short) {
+ return DistanceConverter.kilometresToYards(value);
+ } else if (fromUnit === DISTANCE_UNITS.YARD.short && toUnit === DISTANCE_UNITS.KILOMETRE.short) {
+ return DistanceConverter.yardsToKilometres(value);
+ } else if (fromUnit === DISTANCE_UNITS.MILE.short && toUnit === DISTANCE_UNITS.YARD.short) {
+ return DistanceConverter.milesToYards(value);
+ } else if (fromUnit === DISTANCE_UNITS.YARD.short && toUnit === DISTANCE_UNITS.MILE.short) {
+ return DistanceConverter.yardsToMiles(value);
+ } else if (fromUnit === DISTANCE_UNITS.MILE.short && toUnit === DISTANCE_UNITS.FOOT.short) {
+ return DistanceConverter.milesToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.MILE.short) {
+ return DistanceConverter.feetToMiles(value);
+ } else if (fromUnit === DISTANCE_UNITS.MILE.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.milesToInches(value);
+ } else if (fromUnit === DISTANCE_UNITS.INCH.short && toUnit === DISTANCE_UNITS.MILE.short) {
+ return DistanceConverter.inchesToMiles(value);
+ } else if (fromUnit === DISTANCE_UNITS.YARD.short && toUnit === DISTANCE_UNITS.FOOT.short) {
+ return DistanceConverter.yardsToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.YARD.short) {
+ return DistanceConverter.feetToYards(value);
+ }
+ return conversionErrorString(fromUnit, toUnit);
+};
+var convertUnit = (value, fromUnit, toUnit) => {
+ let result;
+ if (fromUnit === toUnit) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+ if (isMassUnit(fromUnit) && isMassUnit(toUnit)) {
+ result = convertMass(value, fromUnit, toUnit);
+ } else if (isTemperatureUnit(fromUnit) && isTemperatureUnit(toUnit)) {
+ result = convertTemperature(value, fromUnit, toUnit);
+ } else if (isVolumeUnit(fromUnit) && isVolumeUnit(toUnit)) {
+ result = convertVolume(value, fromUnit, toUnit);
+ } else if (isDistanceUnit(fromUnit) && isDistanceUnit(toUnit)) {
+ result = convertDistance(value, fromUnit, toUnit);
+ } else {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+ const rounded = new decimal_default(result).toDecimalPlaces(4);
+ return rounded.isInteger() ? rounded.toNumber() : Number(rounded);
+};
+export {
+ toScientificNotation,
+ hexToRgb,
+ hexToHsl,
+ convertUnixTimestamp,
+ convertUnit,
+ convertIsoString,
+ convertEcmaTimestamp
+};
diff --git a/public/static/style.css b/public/static/style.css
new file mode 100644
index 0000000..953fecc
--- /dev/null
+++ b/public/static/style.css
@@ -0,0 +1,209 @@
+@import "https://unpkg.com/open-props";
+@import "https://unpkg.com/open-props/normalize.min.css";
+@import "https://unpkg.com/open-props/buttons.min.css";
+
+*,
+::after,
+::before {
+ box-sizing: border-box;
+ max-width: 100%;
+}
+
+html {
+ font-family: var(--font-industrial);
+}
+
+body {
+ margin: 0 auto;
+ max-width: clamp(350px, 80vw, 600px);
+
+ @media (max-width: 685px) {
+ max-width: 90vw;
+ }
+}
+
+header#main-header {
+ position: sticky;
+ background-color: var(--surface-1);
+ z-index: 1;
+ top: 0;
+ display: grid;
+ grid-template-columns: 1fr auto;
+ width: 100%;
+ align-items: center;
+
+ h1 {
+ font-size: var(--font-size-fluid-3)
+ }
+
+ button {
+ margin: var(--size-2);
+ }
+}
+
+#main-menu-button {
+ position: relative;
+ anchor-name: --main-menu-button;
+ width: var(--size-12);
+ left: -.5rem;
+}
+
+#main-menu {
+ &:not(:popover-open) {
+ display: none
+ }
+
+ &:popover-open {
+ position-anchor: --main-menu-button;
+ position: fixed;
+ position-area: bottom;
+ width: var(--size-12);
+ background: var(--surface-2);
+ border-radius: var(--radius-2);
+ box-shadow: var(--shadow-3);
+ list-style: none;
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ gap: var(--size-2);
+ margin: var(--size-2) 0 var(--size-2) 0;
+ padding: 0;
+
+ li {
+ margin: 0;
+ padding: 0;
+
+ button {
+ width: 100%;
+ margin: 0;
+ }
+ }
+ }
+}
+
+main {
+ overflow-y: scroll;
+ height: 100%;
+ position: relative;
+ padding-bottom: var(--size-8)
+}
+
+footer#main-footer {
+ width: 100%;
+ background: var(--surface-1);
+ position: fixed;
+ height: var(--size-8);
+ left: 0;
+ bottom: 0;
+ z-index: 10;
+
+ p {
+ position: relative;
+ text-align: center;
+ z-index: 1;
+ margin: var(--size-2) 0;
+ }
+}
+
+h2,
+h3 {
+ text-align: center;
+}
+
+@layer card {
+ .card {
+ border-radius: var(--radius-2);
+ padding: var(--size-fluid-3);
+ box-shadow: var(--shadow-2);
+ margin: 0 auto;
+
+ &:hover {
+ box-shadow: var(--shadow-3);
+ }
+
+ @media (--motionOK) {
+ animation: var(--animation-fade-in);
+ }
+ }
+
+ .card-1 {
+ background: var(--surface-2);
+ }
+
+ .card-2 {
+ background: var(--surface-3);
+ }
+
+ .colour-card {
+ background: var(--gradient-21);
+ }
+}
+
+.form-input {
+ display: grid;
+ gap: var(--size-2);
+ grid-template-rows: 1fr 1fr;
+ align-items: center;
+}
+
+.form-input:has([type="checkbox"]) {
+ grid-template-columns: auto 1fr;
+ grid-template-rows: 1fr;
+}
+
+.full-width {
+ grid-column: 1 / -1;
+}
+
+.form-grid {
+ display: grid;
+ grid-template-columns: auto auto;
+ gap: var(--size-2);
+
+ @media (max-width: 600px) {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ }
+}
+
+fieldset {
+ display: grid;
+ gap: var(--size-2);
+ grid-template-columns: 1fr 1fr;
+
+ @media (max-width: 435px) {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ }
+}
+
+#colour-picker {
+ .form-input {
+ margin: 0 auto;
+ max-width: fit-content;
+ margin-bottom: var(--size-2);
+
+ input {
+ justify-self: center;
+ }
+ }
+
+ #colour-values {
+ margin-inline: var(--size-fluid-3);
+
+ p {
+ display: grid;
+ gap: var(--size-1);
+ grid-template-columns: 1fr 1fr;
+ margin-bottom: var(--size-2);
+ }
+ }
+}
+
+#timestamps {
+ #fieldsets-container {
+ display: grid;
+ grid-template-rows: repeat(2, 1fr);
+ gap: var(--size-2);
+ }
+}
diff --git a/public/timestamps.html b/public/timestamps.html
new file mode 100644
index 0000000..0344c9d
--- /dev/null
+++ b/public/timestamps.html
@@ -0,0 +1,108 @@
+
+ Timestamps
+
+
+
+ ISO String
+
+
+ Enter an ISO 8601 string:
+
+
+
+
+ Convert to:
+
+ UNIX Timestamp
+ ECMAScript Timestamp
+
+
+
+ Convert
+
+
+
diff --git a/public/units.html b/public/units.html
new file mode 100644
index 0000000..4b8250a
--- /dev/null
+++ b/public/units.html
@@ -0,0 +1,139 @@
+
+ Units Converter
+
+
+ Units
+
+
+ Type:
+
+ Mass
+ Temperature
+ Volume
+ Distance
+
+
+
+
+ Amount:
+
+
+
+
+ From:
+
+
+ Grams
+ Kilograms
+ Ounces
+ Pounds
+
+
+
+
+
+ Celsius
+ Celsius (Fan Oven)
+ Fahrenheit
+
+
+
+
+
+ Millilitres
+ Litres
+ Fluid Ounces
+ Pints
+ Gallons
+
+
+
+
+
+ Centimetres
+ Metres
+ Kilometres
+ Inches
+ Feet
+ Miles
+ Yards
+
+
+
+
+
+
+ To:
+
+
+ Grams
+ Kilograms
+ Ounces
+ Pounds
+
+
+
+
+
+ Celsius
+ Celsius (Fan Oven)
+ Fahrenheit
+
+
+
+
+
+ Millilitres
+ Litres
+ Fluid Ounces
+ Pints
+ Gallons
+
+
+
+
+
+ Centimetres
+ Metres
+ Kilometres
+ Inches
+ Feet
+ Miles
+ Yards
+
+
+
+
+
+ Convert
+
+
+ Result:
+
+
+
+
+
diff --git a/src/colour/hex-to-hsl.test.ts b/src/colour/hex-to-hsl.test.ts
new file mode 100644
index 0000000..5d772e0
--- /dev/null
+++ b/src/colour/hex-to-hsl.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it } from 'bun:test';
+import { hexToHsl } from './hex-to-hsl';
+
+describe('hexToHsl', () => {
+ const testCases = [
+ { hex: '#FFFFFF', expected: 'hsl(0, 0%, 100%)' },
+ { hex: '#000000', expected: 'hsl(0, 0%, 0%)' },
+ { hex: '#FF5733', expected: 'hsl(11, 100%, 60%)' },
+ { hex: '#33FF57', expected: 'hsl(131, 100%, 60%)' },
+ { hex: '#3357FF', expected: 'hsl(229, 100%, 60%)' },
+ { hex: '#123456', expected: 'hsl(210, 65%, 20%)' },
+ { hex: '#ABCDEF', expected: 'hsl(210, 68%, 80%)' },
+ ];
+
+ testCases.forEach(({ hex, expected }) => {
+ it(`should convert ${hex} to ${expected}`, () => {
+ expect(hexToHsl(hex)).toBe(expected);
+ });
+ });
+});
diff --git a/src/colour/hex-to-hsl.ts b/src/colour/hex-to-hsl.ts
new file mode 100644
index 0000000..54dff75
--- /dev/null
+++ b/src/colour/hex-to-hsl.ts
@@ -0,0 +1,38 @@
+import Decimal from 'decimal.js';
+
+export const hexToHsl = (hex: string): string => {
+ const red = new Decimal(parseInt(hex.slice(1, 3), 16)).div(255);
+ const green = new Decimal(parseInt(hex.slice(3, 5), 16)).div(255);
+ const blue = new Decimal(parseInt(hex.slice(5, 7), 16)).div(255);
+
+ const cmin = Decimal.min(red, green, blue);
+ const cmax = Decimal.max(red, green, blue);
+ const delta = cmax.minus(cmin);
+
+ let h = new Decimal(0);
+ let s = new Decimal(0);
+ let l = cmax.plus(cmin).div(2);
+
+ if (!delta.isZero()) {
+ if (cmax.equals(red)) {
+ h = green.minus(blue).div(delta).mod(6);
+ } else if (cmax.equals(green)) {
+ h = blue.minus(red).div(delta).plus(2);
+ } else {
+ h = red.minus(green).div(delta).plus(4);
+ }
+ h = h.times(60);
+ if (h.isNegative()) h = h.plus(360);
+ }
+
+ if (!delta.isZero()) {
+ s = delta.div(new Decimal(1).minus((l.times(2).minus(1)).abs()));
+ }
+
+ // Use rounding after all calculations
+ const hFinal = h.toDecimalPlaces(0, Decimal.ROUND_HALF_UP).toNumber();
+ const sFinal = s.times(100).toDecimalPlaces(0, Decimal.ROUND_HALF_UP).toNumber();
+ const lFinal = l.times(100).toDecimalPlaces(0, Decimal.ROUND_HALF_UP).toNumber();
+
+ return `hsl(${hFinal}, ${sFinal}%, ${lFinal}%)`;
+};
diff --git a/src/colour/hex-to-rgb.test.ts b/src/colour/hex-to-rgb.test.ts
new file mode 100644
index 0000000..b0f4e08
--- /dev/null
+++ b/src/colour/hex-to-rgb.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it } from 'bun:test';
+import { hexToRgb } from './hex-to-rgb';
+
+describe('hexToRgb', () => {
+ const testCases = [
+ { hex: '#FFFFFF', expected: 'rgb(255, 255, 255)' },
+ { hex: '#000000', expected: 'rgb(0, 0, 0)' },
+ { hex: '#FF5733', expected: 'rgb(255, 87, 51)' },
+ { hex: '#33FF57', expected: 'rgb(51, 255, 87)' },
+ { hex: '#3357FF', expected: 'rgb(51, 87, 255)' },
+ { hex: '#123456', expected: 'rgb(18, 52, 86)' },
+ { hex: '#ABCDEF', expected: 'rgb(171, 205, 239)' },
+ ];
+
+ testCases.forEach(({ hex, expected }) => {
+ it(`should convert ${hex} to ${expected}`, () => {
+ expect(hexToRgb(hex)).toBe(expected);
+ });
+ });
+});
diff --git a/src/colour/hex-to-rgb.ts b/src/colour/hex-to-rgb.ts
new file mode 100644
index 0000000..ac96d7b
--- /dev/null
+++ b/src/colour/hex-to-rgb.ts
@@ -0,0 +1,7 @@
+export const hexToRgb = (hex: string): string => {
+ const red = parseInt(hex.slice(1, 3), 16);
+ const green = parseInt(hex.slice(3, 5), 16);
+ const blue = parseInt(hex.slice(5, 7), 16);
+ return `rgb(${red}, ${green}, ${blue})`;
+};
+
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..5694bfb
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,7 @@
+export { convertEcmaTimestamp } from './timestamps/ecma';
+export { convertUnixTimestamp } from './timestamps/unix';
+export { convertIsoString } from './timestamps/iso-string';
+export { hexToRgb } from './colour/hex-to-rgb';
+export { hexToHsl } from './colour/hex-to-hsl';
+export { toScientificNotation } from './numbers/scientific-notation';
+export { convertUnit } from './units/convert-unit';
diff --git a/src/numbers/scientific-notation.test.ts b/src/numbers/scientific-notation.test.ts
new file mode 100644
index 0000000..663ab9b
--- /dev/null
+++ b/src/numbers/scientific-notation.test.ts
@@ -0,0 +1,49 @@
+import {describe, expect, test} from 'bun:test'
+import { toScientificNotation } from './scientific-notation';
+
+describe("toScientificNotation", () => {
+ test("should convert numbers to exponential HTML format without decimals", () => {
+ // Arrange
+ const input = 345600000000;
+
+ // Act
+ const result = toScientificNotation(input);
+
+ // Assert
+ expect(result).toBe("3 × 1011 ");
+ });
+
+ test("should convert numbers to exponential HTML format to specified precision", () => {
+ // Arrange
+ const input = 345600000000;
+ const precision = 4;
+
+ // Act
+ const result = toScientificNotation(input, precision);
+
+ // Assert
+ expect(result).toBe("3.4560 × 1011 ");
+ });
+
+ test("should handle negative exponents", () => {
+ // Arrange
+ const input = 0.00003456;
+
+ // Act
+ const result = toScientificNotation(input);
+
+ // Assert
+ expect(result).toBe("3 × 10-5 ");
+ });
+
+ test("should fallback to the original string if parsing fails", () => {
+ // Arrange
+ const input = "not a number";
+
+ // Act
+ const result = toScientificNotation(input as unknown as number);
+
+ // Assert
+ expect(result).toBe("not a number");
+ });
+});
diff --git a/src/numbers/scientific-notation.ts b/src/numbers/scientific-notation.ts
new file mode 100644
index 0000000..0f1ca9d
--- /dev/null
+++ b/src/numbers/scientific-notation.ts
@@ -0,0 +1,20 @@
+export const toScientificNotation = (num: number, precision = 0): string => {
+ // Convert to exponential notation
+ const exponential = precision > 0 ? Number(num).toExponential(precision) : Number(num).toExponential(0);
+
+ // Match both integer and decimal coefficients
+ const match = exponential.match(/^(-?\d+(?:\.\d+)?)e([+-]?\d+)$/);
+
+ if (!match) {
+ return num.toString();
+ }
+
+ let coefficient = match[1];
+ const exponent = parseInt(match[2], 10).toString();
+
+ if (precision === 0) {
+ coefficient = Math.round(Number(coefficient)).toString();
+ }
+
+ return `${coefficient} × 10${exponent} `;
+};
diff --git a/src/timestamps/ecma.test.ts b/src/timestamps/ecma.test.ts
new file mode 100644
index 0000000..1b969d3
--- /dev/null
+++ b/src/timestamps/ecma.test.ts
@@ -0,0 +1,113 @@
+import { describe, expect, it } from "bun:test";
+import { convertEcmaTimestamp } from "./ecma";
+
+describe('convertEcmaTimestamp', () => {
+ const ivalidTimestamps = [
+ 0,
+ null,
+ undefined,
+ 'blorgen',
+ ];
+ ivalidTimestamps.forEach((invalidTimestamp) => {
+ it('should return "Please enter a valid timestamp" for invalid input', () => {
+ // Arrange
+ const conversionOption = 'iso';
+
+ // Act
+ const result = convertEcmaTimestamp(invalidTimestamp as number, conversionOption);
+
+ // Assert
+ expect(result).toBe('Please enter a valid timestamp');
+ });
+ });
+
+ it('should convert a valid ECMA timestamp to ISO string', () => {
+ // Arrange
+ const ecmaTimestamp = 1633046400000;
+ const conversionOption = 'iso';
+ const expectedIsoString = '2021-10-01T00:00:00.000Z';
+
+ // Act
+ const result = convertEcmaTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedIsoString);
+ });
+
+ it('should convert a valid ECMA timestamp to UTC string', () => {
+ // Arrange
+ const ecmaTimestamp = 1633046400000;
+ const conversionOption = 'utc';
+ const expectedUtcString = 'Fri, 01 Oct 2021 00:00:00 GMT';
+
+ // Act
+ const result = convertEcmaTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedUtcString);
+ });
+
+ it('should convert a valid ECMA timestamp to Locale string', () => {
+ // Arrange
+ const ecmaTimestamp = 1633046400000;
+ const conversionOption = 'locale';
+ const expectedLocaleString = '10/1/2021, 12:00:00 AM'; // Without a browser the default local is en-US
+
+ // Act
+ const result = convertEcmaTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedLocaleString);
+ });
+
+ it('should convert a valid ECMA timestamp to seconds', () => {
+ // Arrange
+ const ecmaTimestamp = 1633046400000;
+ const conversionOption = 'seconds';
+ const expectedSeconds = 1633046400;
+
+ // Act
+ const result = convertEcmaTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedSeconds);
+ });
+
+ it('should convert a valid ECMA timestamp to hours', () => {
+ // Arrange
+ const ecmaTimestamp = 7200000; // 2 hours in milliseconds
+ const conversionOption = 'hours';
+ const expectedHours = 2;
+
+ // Act
+ const result = convertEcmaTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedHours);
+ });
+
+ it('should convert a valid ECMA timestamp to days', () => {
+ // Arrange
+ const ecmaTimestamp = 172800000; // 2 days in milliseconds
+ const conversionOption = 'days';
+ const expectedDays = 2;
+
+ // Act
+ const result = convertEcmaTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedDays);
+ });
+
+ it('should return "Invalid conversion option" for an invalid conversion option', () => {
+ // Arrange
+ const ecmaTimestamp = 1633046400000;
+ const conversionOption = 'invalid-option' as any;
+
+ // Act
+ const result = convertEcmaTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe('Invalid conversion option');
+ });
+});
diff --git a/src/timestamps/ecma.ts b/src/timestamps/ecma.ts
new file mode 100644
index 0000000..a540ee0
--- /dev/null
+++ b/src/timestamps/ecma.ts
@@ -0,0 +1,34 @@
+type EcmaConversionOption = 'iso' | 'utc' | 'locale' | 'seconds' | 'hours' | 'days';
+
+const getHoursFromMilliseconds = (milliseconds: number): number => {
+ return Math.floor(milliseconds / (1000 * 60 * 60));
+};
+
+const getDaysFromMilliseconds = (milliseconds: number): number => {
+ return Math.floor(milliseconds / (1000 * 60 * 60 * 24));
+};
+
+export const convertEcmaTimestamp = (ecmaTimestamp: number, ecmaConversionOption: EcmaConversionOption): string | number => {
+ if (!ecmaTimestamp || isNaN(ecmaTimestamp)) {
+ return 'Please enter a valid timestamp';
+ }
+
+ const date = new Date(ecmaTimestamp);
+
+ switch (ecmaConversionOption) {
+ case 'iso':
+ return date.toISOString();
+ case 'utc':
+ return date.toUTCString();
+ case 'locale':
+ return date.toLocaleString();
+ case 'seconds':
+ return Math.floor(ecmaTimestamp / 1000);
+ case 'hours':
+ return getHoursFromMilliseconds(ecmaTimestamp);
+ case 'days':
+ return getDaysFromMilliseconds(ecmaTimestamp);
+ default:
+ return 'Invalid conversion option';
+ }
+};
diff --git a/src/timestamps/iso-string.test.ts b/src/timestamps/iso-string.test.ts
new file mode 100644
index 0000000..33e2861
--- /dev/null
+++ b/src/timestamps/iso-string.test.ts
@@ -0,0 +1,83 @@
+import { describe, expect, it } from "bun:test";
+import { convertIsoString } from "./iso-string";
+
+describe('convertIsoString', () => {
+ const invalidIsoStringPatterns = [
+ null,
+ undefined,
+ '',
+ 'not-an-iso-string',
+ '2021-12-01 23:59:59Z', // Missing 'T'
+ ];
+ invalidIsoStringPatterns.forEach((invalidIsoString) => {
+ it(`should return "Please enter a valid ISO string" for input that doesn't match the regex pattern "${invalidIsoString}"`, () => {
+ // Arrange
+ const conversionType = 'unix';
+
+ // Act
+ const result = convertIsoString(invalidIsoString as string, conversionType);
+
+ // Assert
+ expect(result).toBe('Please enter a valid ISO string');
+ });
+ });
+
+ const invalidIsoStrings = [
+ '2021-13-01T00:00:00Z', // Invalid month
+ '2021-00-01T00:00:00Z', // Invalid month
+ '2021-12-32T00:00:00Z', // Invalid day
+ '2021-12-01T24:00:00Z', // Invalid hour
+ '2021-12-01T00:60:00Z', // Invalid minute
+ '2021-12-01T00:00:60Z', // Invalid second
+ ];
+ invalidIsoStringPatterns.forEach((invalidIsoString) => {
+ it(`should return "Invalid ISO String" for input that has invalid properties "${invalidIsoString}"`, () => {
+ // Arrange
+ const conversionType = 'unix';
+
+ // Act
+ const result = convertIsoString(invalidIsoString as string, conversionType);
+
+ // Assert
+ expect(result).toBe('Please enter a valid ISO string');
+ });
+ });
+
+ it('should convert a valid ISO string to UNIX timestamp', () => {
+ // Arrange
+ const isoString = '2021-10-01T00:00:00Z';
+ const conversionType = 'unix';
+ const expectedUnixTimestamp = 1633046400;
+
+ // Act
+ const result = convertIsoString(isoString, conversionType);
+
+ // Assert
+ expect(result).toBe(expectedUnixTimestamp);
+ });
+
+ it('should convert a valid ISO string to ECMA timestamp', () => {
+ // Arrange
+ const isoString = '2021-10-01T00:00:00Z';
+ const conversionType = 'ecma';
+ const expectedEcmaTimestamp = 1633046400000;
+
+ // Act
+ const result = convertIsoString(isoString, conversionType);
+
+ // Assert
+ expect(result).toBe(expectedEcmaTimestamp);
+ });
+
+ it('should return "Invalid conversion type" for an unsupported conversion type', () => {
+ // Arrange
+ const isoString = '2021-10-01T00:00:00Z';
+ const conversionType = 'invalid-type' as 'unix';
+
+ // Act
+ const result = convertIsoString(isoString, conversionType);
+
+ // Assert
+ expect(result).toBe('Invalid conversion type');
+ });
+});
diff --git a/src/timestamps/iso-string.ts b/src/timestamps/iso-string.ts
new file mode 100644
index 0000000..f42f086
--- /dev/null
+++ b/src/timestamps/iso-string.ts
@@ -0,0 +1,22 @@
+type IsoStringConversions = 'unix' | 'ecma';
+
+export const convertIsoString = (isoString: string, conversionType: IsoStringConversions): number | string => {
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
+ if (!isoString || !isoDateRegex.test(isoString)) {
+ return 'Please enter a valid ISO string';
+ }
+
+ const date = new Date(isoString);
+ if (isNaN(date.getTime())) {
+ return 'Invalid ISO string';
+ }
+
+ switch (conversionType) {
+ case 'unix':
+ return Math.floor(date.getTime() / 1000);
+ case 'ecma':
+ return date.getTime();
+ default:
+ return 'Invalid conversion type';
+ }
+};
diff --git a/src/timestamps/unix.test.ts b/src/timestamps/unix.test.ts
new file mode 100644
index 0000000..6276755
--- /dev/null
+++ b/src/timestamps/unix.test.ts
@@ -0,0 +1,114 @@
+import { describe, expect, it } from "bun:test";
+
+import { convertUnixTimestamp } from "./unix";
+
+describe('convertUnixTimestamp', () => {
+ const ivalidTimestamps = [
+ 0,
+ null,
+ undefined,
+ 'blorgen',
+ ];
+ ivalidTimestamps.forEach((invalidTimestamp) => {
+ it('should return "Please enter a valid timestamp" for invalid input', () => {
+ // Arrange
+ const conversionOption = 'iso';
+
+ // Act
+ const result = convertUnixTimestamp(invalidTimestamp as number, conversionOption);
+
+ // Assert
+ expect(result).toBe('Please enter a valid timestamp');
+ });
+ });
+
+ it('should convert a valid UNIX timestamp to ISO string', () => {
+ // Arrange
+ const ecmaTimestamp = 1633046400;
+ const conversionOption = 'iso';
+ const expectedIsoString = '2021-10-01T00:00:00.000Z';
+
+ // Act
+ const result = convertUnixTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedIsoString);
+ });
+
+ it('should convert a valid UNIX timestamp to UTC string', () => {
+ // Arrange
+ const ecmaTimestamp = 1633046400;
+ const conversionOption = 'utc';
+ const expectedUtcString = 'Fri, 01 Oct 2021 00:00:00 GMT';
+
+ // Act
+ const result = convertUnixTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedUtcString);
+ });
+
+ it('should convert a valid UNIX timestamp to Locale string', () => {
+ // Arrange
+ const ecmaTimestamp = 1633046400;
+ const conversionOption = 'locale';
+ const expectedLocaleString = '10/1/2021, 12:00:00 AM'; // Without a browser the default local is en-US
+
+ // Act
+ const result = convertUnixTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedLocaleString);
+ });
+
+ it('should convert a valid UNIX timestamp to milliseconds', () => {
+ // Arrange
+ const ecmaTimestamp = 163304640;
+ const conversionOption = 'milliseconds';
+ const expectedSeconds = 163304640000;
+
+ // Act
+ const result = convertUnixTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedSeconds);
+ });
+
+ it('should convert a valid UNIX timestamp to hours', () => {
+ // Arrange
+ const ecmaTimestamp = 7200; // 2 hours in seconds
+ const conversionOption = 'hours';
+ const expectedHours = 2;
+
+ // Act
+ const result = convertUnixTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedHours);
+ });
+
+ it('should convert a valid UNIX timestamp to days', () => {
+ // Arrange
+ const ecmaTimestamp = 172800; // 2 days in seconds
+ const conversionOption = 'days';
+ const expectedDays = 2;
+
+ // Act
+ const result = convertUnixTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe(expectedDays);
+ });
+
+ it('should return "Invalid conversion option" for an invalid conversion option', () => {
+ // Arrange
+ const ecmaTimestamp = 1633046400;
+ const conversionOption = 'invalid-option' as any;
+
+ // Act
+ const result = convertUnixTimestamp(ecmaTimestamp, conversionOption);
+
+ // Assert
+ expect(result).toBe('Invalid conversion option');
+ });
+});
diff --git a/src/timestamps/unix.ts b/src/timestamps/unix.ts
new file mode 100644
index 0000000..26c41d9
--- /dev/null
+++ b/src/timestamps/unix.ts
@@ -0,0 +1,34 @@
+type UnixConversionOption = 'iso' | 'utc' | 'locale' | 'milliseconds' | 'hours' | 'days';
+
+const getHoursFromSeconds = (seconds: number): number => {
+ return Math.floor(seconds / (60 * 60));
+};
+
+const getDaysFromSeconds = (seconds: number): number => {
+ return Math.floor(seconds / (60 * 60 * 24));
+};
+
+export const convertUnixTimestamp = (unixTimestamp: number, conversionOption: UnixConversionOption): string | number => {
+ if (!unixTimestamp || isNaN(unixTimestamp)) {
+ return 'Please enter a valid timestamp';
+ }
+
+ const date = new Date(unixTimestamp * 1000);
+
+ switch (conversionOption) {
+ case 'iso':
+ return date.toISOString();
+ case 'utc':
+ return date.toUTCString();
+ case 'locale':
+ return date.toLocaleString();
+ case 'milliseconds':
+ return date.getTime();
+ case 'hours':
+ return getHoursFromSeconds(date.getTime() / 1000);
+ case 'days':
+ return getDaysFromSeconds(date.getTime() / 1000);
+ default:
+ return 'Invalid conversion option';
+ }
+};
diff --git a/src/units/convert-unit.test.ts b/src/units/convert-unit.test.ts
new file mode 100644
index 0000000..6111e45
--- /dev/null
+++ b/src/units/convert-unit.test.ts
@@ -0,0 +1,109 @@
+import { describe, expect, test } from 'bun:test';
+import { convertUnit } from './convert-unit';
+import type { UnitShort } from './model';
+
+describe('convertUnit', () => {
+ test('should return an error string if the from and to units are the same', () => {
+ const testCases = [
+ 'm',
+ 'km',
+ 'ft',
+ 'yd',
+ 'mi',
+ 'cm',
+ 'mm',
+ 'in',
+ 'f',
+ 'c',
+ 'c (fan)',
+ 'k',
+ 'kg',
+ 'g',
+ 'lb',
+ 'oz',
+ 'l',
+ 'ml',
+ 'gal',
+ 'pt',
+ ];
+
+ testCases.forEach((unit) => {
+ // Arrange
+ const value = 100;
+ const expectedErrorString = `Cannot convert from ${unit} to ${unit}`;
+
+ // Act
+ const result = convertUnit(value, unit, unit);
+
+ // Assert
+ expect(result).toBe(expectedErrorString);
+ });
+ });
+
+ test('should return an error string if the from or to unit is not supported', () => {
+ const testCases = [
+ { value: 100, fromUnit: 'm', toUnit: 'xyz' },
+ { value: 50, fromUnit: 'abc', toUnit: 'km' },
+ { value: 75, fromUnit: 'foo', toUnit: 'bar' },
+ ];
+
+ testCases.forEach(({ value, fromUnit, toUnit }) => {
+ // Arrange
+ const expectedErrorString = `Cannot convert from ${fromUnit} to ${toUnit}`;
+
+ // Act
+ const result = convertUnit(value, fromUnit as UnitShort, toUnit as UnitShort);
+
+ // Assert
+ expect(result).toBe(expectedErrorString);
+ });
+ });
+
+ test('should return an error string if the units an incompatible', () => {
+ const testCases = [
+ { value: 100, fromUnit: 'm', toUnit: 'kg' },
+ { value: 50, fromUnit: 'c', toUnit: 'l' },
+ { value: 75, fromUnit: 'g', toUnit: 'ft' },
+ ];
+
+ testCases.forEach(({ value, fromUnit, toUnit }) => {
+ // Arrange
+ const expectedErrorString = `Cannot convert from ${fromUnit} to ${toUnit}`;
+
+ // Act
+ const result = convertUnit(value, fromUnit as UnitShort, toUnit as UnitShort);
+
+ // Assert
+ expect(result).toBe(expectedErrorString);
+ });
+ });
+
+ test('should correctly convert between supported units', () => {
+ const testCases = [
+ { value: 1000, fromUnit: 'g', toUnit: 'kg', expected: 1 },
+ { value: 5, fromUnit: 'kg', toUnit: 'g', expected: 5000 },
+ { value: 16, fromUnit: 'oz', toUnit: 'lb', expected: 1 },
+ { value: 2.20462, fromUnit: 'lb', toUnit: 'kg', expected: 1 },
+ { value: 100, fromUnit: 'cm', toUnit: 'm', expected: 1 },
+ { value: 1, fromUnit: 'km', toUnit: 'm', expected: 1000 },
+ { value: 39.3701, fromUnit: 'in', toUnit: 'm', expected: 1 },
+ { value: 3.28084, fromUnit: 'ft', toUnit: 'm', expected: 1 },
+ { value: 1.09361, fromUnit: 'yd', toUnit: 'm', expected: 1 },
+ { value: 0.000621371, fromUnit: 'mi', toUnit: 'm', expected: 1 },
+ { value: 1000, fromUnit: 'ml', toUnit: 'l', expected: 1 },
+ { value: 3.78541, fromUnit: 'l', toUnit: 'gal', expected: 1 },
+ { value: 8, fromUnit: 'pt', toUnit: 'l', expected: 4.54609 },
+ { value: 32, fromUnit: 'f', toUnit: 'c', expected: 0 },
+ { value: 100, fromUnit: 'c', toUnit: 'f', expected: 212 },
+ { value: 180, fromUnit: 'c (fan)', toUnit: 'f', expected: 392 },
+ ];
+
+ testCases.forEach(({ value, fromUnit, toUnit, expected }) => {
+ // Act
+ const result = convertUnit(value, fromUnit as UnitShort, toUnit as UnitShort);
+
+ // Assert
+ expect(result).toBe(expected);
+ });
+ });
+});
diff --git a/src/units/convert-unit.ts b/src/units/convert-unit.ts
new file mode 100644
index 0000000..4e5654e
--- /dev/null
+++ b/src/units/convert-unit.ts
@@ -0,0 +1,211 @@
+import { TemperatureConverter } from "./temperatre-converter";
+import { MassConverter } from "./mass-converter";
+import { VolumeConverter } from "./volume-converter";
+import { MASS_UNITS, VOLUME_UNITS, TEMPERATURE_UNITS, type UnitShort, isMassUnit, isTemperatureUnit, isVolumeUnit, isDistanceUnit, DISTANCE_UNITS } from "./model";
+import Decimal from "decimal.js";
+import { DistanceConverter } from "./distance-converter";
+
+const conversionErrorString = (fromUnit: UnitShort, toUnit: UnitShort): string => {
+ return `Cannot convert from ${fromUnit} to ${toUnit}`;
+}
+
+const convertMass = (value: number, fromUnit: UnitShort, toUnit: UnitShort): number | string => {
+ if (!isMassUnit(toUnit) || !isMassUnit(fromUnit)) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+
+ if (fromUnit === MASS_UNITS.KILOGRAM.short && toUnit === MASS_UNITS.POUND.short) {
+ return MassConverter.kilogramsToPounds(value);
+ } else if (fromUnit === MASS_UNITS.POUND.short && toUnit === MASS_UNITS.KILOGRAM.short) {
+ return MassConverter.poundsToKilograms(value);
+ } else if (fromUnit === MASS_UNITS.GRAM.short && toUnit === MASS_UNITS.KILOGRAM.short) {
+ return MassConverter.gramsToKilograms(value);
+ } else if (fromUnit === MASS_UNITS.KILOGRAM.short && toUnit === MASS_UNITS.GRAM.short) {
+ return MassConverter.kilogramsToGrams(value);
+ } else if (fromUnit === MASS_UNITS.GRAM.short && toUnit === MASS_UNITS.OUNCE.short) {
+ return MassConverter.gramsToOunces(value);
+ } else if (fromUnit === MASS_UNITS.OUNCE.short && toUnit === MASS_UNITS.GRAM.short) {
+ return MassConverter.ouncesToGrams(value);
+ } else if (fromUnit === MASS_UNITS.OUNCE.short && toUnit === MASS_UNITS.POUND.short) {
+ return MassConverter.ouncesToPounds(value);
+ } else if (fromUnit === MASS_UNITS.POUND.short && toUnit === MASS_UNITS.OUNCE.short) {
+ return MassConverter.poundsToOunces(value);
+ } else if (fromUnit === MASS_UNITS.GRAM.short && toUnit === MASS_UNITS.POUND.short) {
+ return MassConverter.gramsToPounds(value);
+ } else if (fromUnit === MASS_UNITS.POUND.short && toUnit === MASS_UNITS.GRAM.short) {
+ return MassConverter.poundsToGrams(value);
+ } else if (fromUnit === MASS_UNITS.KILOGRAM.short && toUnit === MASS_UNITS.OUNCE.short) {
+ return MassConverter.kilogramsToOunces(value);
+ } else if (fromUnit === MASS_UNITS.OUNCE.short && toUnit === MASS_UNITS.KILOGRAM.short) {
+ return MassConverter.ouncesToKilograms(value);
+ }
+
+ return conversionErrorString(fromUnit, toUnit);
+}
+
+const convertTemperature = (value: number, fromUnit: UnitShort, toUnit: UnitShort): number | string => {
+ if (!isTemperatureUnit(toUnit) || !isTemperatureUnit(fromUnit)) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+
+ if (fromUnit === TEMPERATURE_UNITS.CELSIUS.short && toUnit === TEMPERATURE_UNITS.FAHRENHEIT.short) {
+ return TemperatureConverter.celsiusToFahrenheit(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.FAHRENHEIT.short && toUnit === TEMPERATURE_UNITS.CELSIUS.short) {
+ return TemperatureConverter.fahrenheitToCelsius(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.CELSIUS_FAN_OVEN.short && toUnit === TEMPERATURE_UNITS.FAHRENHEIT.short) {
+ return TemperatureConverter.celsiusFanOvenToFahrenheit(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.FAHRENHEIT.short && toUnit === TEMPERATURE_UNITS.CELSIUS_FAN_OVEN.short) {
+ return TemperatureConverter.fahrenheitToCelsiusFanOven(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.CELSIUS.short && toUnit === TEMPERATURE_UNITS.CELSIUS_FAN_OVEN.short) {
+ return TemperatureConverter.celsiusToCelsiusFanOven(value);
+ } else if (fromUnit === TEMPERATURE_UNITS.CELSIUS_FAN_OVEN.short && toUnit === TEMPERATURE_UNITS.CELSIUS.short) {
+ return TemperatureConverter.celsiusFanOvenToCelsius(value);
+ }
+
+ return conversionErrorString(fromUnit, toUnit);
+}
+
+const convertVolume = (value: number, fromUnit: UnitShort, toUnit: UnitShort): number | string => {
+ if (!isVolumeUnit(toUnit) || !isVolumeUnit(fromUnit)) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+
+ if (fromUnit === VOLUME_UNITS.LITRE.short && toUnit === VOLUME_UNITS.PINT.short) {
+ return VolumeConverter.litresToPints(value);
+ } else if (fromUnit === VOLUME_UNITS.PINT.short && toUnit === VOLUME_UNITS.LITRE.short) {
+ return VolumeConverter.pintsToLitres(value);
+ } else if (fromUnit === VOLUME_UNITS.LITRE.short && toUnit === VOLUME_UNITS.FLUID_OUNCE.short) {
+ return VolumeConverter.litresToFluidOunces(value);
+ } else if (fromUnit === VOLUME_UNITS.FLUID_OUNCE.short && toUnit === VOLUME_UNITS.LITRE.short) {
+ return VolumeConverter.fluidOuncesToLitres(value);
+ } else if (fromUnit === VOLUME_UNITS.GALLON.short && toUnit === VOLUME_UNITS.PINT.short) {
+ return VolumeConverter.gallonsToPints(value);
+ } else if (fromUnit === VOLUME_UNITS.PINT.short && toUnit === VOLUME_UNITS.GALLON.short) {
+ return VolumeConverter.pintsToGallons(value);
+ } else if (fromUnit === VOLUME_UNITS.GALLON.short && toUnit === VOLUME_UNITS.MILLILITRE.short) {
+ return VolumeConverter.gallonsToMillilitres(value);
+ } else if (fromUnit === VOLUME_UNITS.MILLILITRE.short && toUnit === VOLUME_UNITS.GALLON.short) {
+ return VolumeConverter.millilitresToGallons(value);
+ } else if (fromUnit === VOLUME_UNITS.PINT.short && toUnit === VOLUME_UNITS.FLUID_OUNCE.short) {
+ return VolumeConverter.pintsToFluidOunces(value);
+ } else if (fromUnit === VOLUME_UNITS.FLUID_OUNCE.short && toUnit === VOLUME_UNITS.PINT.short) {
+ return VolumeConverter.fluidOuncesToPints(value);
+ } else if (fromUnit === VOLUME_UNITS.MILLILITRE.short && toUnit === VOLUME_UNITS.FLUID_OUNCE.short) {
+ return VolumeConverter.millilitresToFluidOunces(value);
+ } else if (fromUnit === VOLUME_UNITS.FLUID_OUNCE.short && toUnit === VOLUME_UNITS.MILLILITRE.short) {
+ return VolumeConverter.fluidOuncesToMillilitres(value);
+ } else if (fromUnit === VOLUME_UNITS.LITRE.short && toUnit === VOLUME_UNITS.GALLON.short) {
+ return VolumeConverter.litresToGallons(value);
+ } else if (fromUnit === VOLUME_UNITS.LITRE.short && toUnit === VOLUME_UNITS.MILLILITRE.short) {
+ return VolumeConverter.litresToMillilitres(value);
+ } else if (fromUnit === VOLUME_UNITS.MILLILITRE.short && toUnit === VOLUME_UNITS.LITRE.short) {
+ return VolumeConverter.millilitresToLitres(value);
+ } else if (fromUnit === VOLUME_UNITS.GALLON.short && toUnit === VOLUME_UNITS.LITRE.short) {
+ return VolumeConverter.gallonsToLitres(value);
+ } else if (fromUnit === VOLUME_UNITS.MILLILITRE.short && toUnit === VOLUME_UNITS.PINT.short) {
+ return VolumeConverter.millilitresToPints(value);
+ } else if (fromUnit === VOLUME_UNITS.PINT.short && toUnit === VOLUME_UNITS.MILLILITRE.short) {
+ return VolumeConverter.pintsToMillilitres(value);
+ } else if (fromUnit === VOLUME_UNITS.GALLON.short && toUnit === VOLUME_UNITS.FLUID_OUNCE.short) {
+ return VolumeConverter.gallonsToFluidOunces(value);
+ } else if (fromUnit === VOLUME_UNITS.FLUID_OUNCE.short && toUnit === VOLUME_UNITS.GALLON.short) {
+ return VolumeConverter.fluidOuncesToGallons(value);
+ }
+
+ return conversionErrorString(fromUnit, toUnit);
+}
+
+const convertDistance = (value: number, fromUnit: UnitShort, toUnit: UnitShort): number | string => {
+ if (!isDistanceUnit(toUnit) || !isDistanceUnit(fromUnit)) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+
+ if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.YARD.short) {
+ return DistanceConverter.metresToYards(value);
+ } else if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.FOOT.short) {
+ return DistanceConverter.metresToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.feetToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.YARD.short && toUnit === DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.yardsToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.KILOMETRE.short && toUnit === DISTANCE_UNITS.MILE.short) {
+ return DistanceConverter.kilometresToMiles(value);
+ } else if (fromUnit === DISTANCE_UNITS.MILE.short && toUnit === DISTANCE_UNITS.KILOMETRE.short) {
+ return DistanceConverter.milesToKilometres(value);
+ } else if (fromUnit === DISTANCE_UNITS.CENTIMETRE.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.centimetresToInches(value);
+ } else if (fromUnit === DISTANCE_UNITS.CENTIMETRE.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.centimetresToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.CENTIMETRE.short) {
+ return DistanceConverter.feetToCentimetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.INCH.short && toUnit === DISTANCE_UNITS.CENTIMETRE.short) {
+ return DistanceConverter.inchesToCentimetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.feetToInches(value);
+ } else if (fromUnit === DISTANCE_UNITS.INCH.short && toUnit === DISTANCE_UNITS.FOOT.short) {
+ return DistanceConverter.inchesToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.CENTIMETRE.short && toUnit == DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.centimetresToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.CENTIMETRE.short) {
+ return DistanceConverter.metresToCentimetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.KILOMETRE.short && toUnit === DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.kilometresToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.KILOMETRE.short) {
+ return DistanceConverter.metresToKilometres(value);
+ } else if (fromUnit === DISTANCE_UNITS.KILOMETRE.short && toUnit === DISTANCE_UNITS.CENTIMETRE.short) {
+ return DistanceConverter.kilometresToCentimetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.CENTIMETRE.short && toUnit === DISTANCE_UNITS.KILOMETRE.short) {
+ return DistanceConverter.centimetresToKilometres(value);
+ } else if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.metresToInches(value);
+ } else if (fromUnit === DISTANCE_UNITS.INCH.short && toUnit === DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.inchesToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.KILOMETRE.short && toUnit === DISTANCE_UNITS.YARD.short) {
+ return DistanceConverter.kilometresToYards(value);
+ } else if (fromUnit === DISTANCE_UNITS.YARD.short && toUnit === DISTANCE_UNITS.KILOMETRE.short) {
+ return DistanceConverter.yardsToKilometres(value);
+ } else if (fromUnit === DISTANCE_UNITS.MILE.short && toUnit === DISTANCE_UNITS.YARD.short) {
+ return DistanceConverter.milesToYards(value);
+ } else if (fromUnit === DISTANCE_UNITS.YARD.short && toUnit === DISTANCE_UNITS.MILE.short) {
+ return DistanceConverter.yardsToMiles(value);
+ } else if (fromUnit === DISTANCE_UNITS.MILE.short && toUnit === DISTANCE_UNITS.FOOT.short) {
+ return DistanceConverter.milesToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.MILE.short) {
+ return DistanceConverter.feetToMiles(value);
+ } else if (fromUnit === DISTANCE_UNITS.MILE.short && toUnit === DISTANCE_UNITS.INCH.short) {
+ return DistanceConverter.milesToInches(value);
+ } else if (fromUnit === DISTANCE_UNITS.INCH.short && toUnit === DISTANCE_UNITS.MILE.short) {
+ return DistanceConverter.inchesToMiles(value);
+ } else if (fromUnit === DISTANCE_UNITS.YARD.short && toUnit === DISTANCE_UNITS.FOOT.short) {
+ return DistanceConverter.yardsToFeet(value);
+ } else if (fromUnit === DISTANCE_UNITS.FOOT.short && toUnit === DISTANCE_UNITS.YARD.short) {
+ return DistanceConverter.feetToYards(value);
+ } else if (fromUnit === DISTANCE_UNITS.MILE.short && toUnit === DISTANCE_UNITS.METRE.short) {
+ return DistanceConverter.milesToMetres(value);
+ } else if (fromUnit === DISTANCE_UNITS.METRE.short && toUnit === DISTANCE_UNITS.MILE.short) {
+ return DistanceConverter.metresToMiles(value);
+ }
+
+ return conversionErrorString(fromUnit, toUnit);
+}
+
+export const convertUnit = (value: number, fromUnit: UnitShort, toUnit: UnitShort): number | string => {
+ let result: number | string;
+ if (fromUnit === toUnit) {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+ if (isMassUnit(fromUnit) && isMassUnit(toUnit)) {
+ result = convertMass(value, fromUnit, toUnit);
+ } else if (isTemperatureUnit(fromUnit) && isTemperatureUnit(toUnit)) {
+ result = convertTemperature(value, fromUnit, toUnit);
+ } else if (isVolumeUnit(fromUnit) && isVolumeUnit(toUnit)) {
+ result = convertVolume(value, fromUnit, toUnit);
+ } else if (isDistanceUnit(fromUnit) && isDistanceUnit(toUnit)) {
+ result = convertDistance(value, fromUnit, toUnit);
+ } else {
+ return conversionErrorString(fromUnit, toUnit);
+ }
+
+ const rounded = new Decimal(result).toDecimalPlaces(5);
+ return rounded.isInteger() ? rounded.toNumber() : Number(rounded);
+}
diff --git a/src/units/distance-converter.test.ts b/src/units/distance-converter.test.ts
new file mode 100644
index 0000000..12acadf
--- /dev/null
+++ b/src/units/distance-converter.test.ts
@@ -0,0 +1,580 @@
+import { describe, expect, test } from 'bun:test';
+import { DistanceConverter } from './distance-converter';
+
+describe('DistanceConverter', () => {
+ describe('metresToYards', () => {
+ const testCases = [
+ [1, 1.09361],
+ [10, 10.9361],
+ [20, 21.8722],
+ [30, 32.8083]
+ ];
+ testCases.forEach(([metres, yards]) => {
+ test(`should convert ${metres} metres to ${yards} yards`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.metresToYards(metres);
+
+ // Assert
+ expect(result).toEqual(yards);
+ });
+ });
+ });
+
+ describe('metresToFeet', () => {
+ const testCases = [
+ [1, 3.28084],
+ [10, 32.8084],
+ [20, 65.6168],
+ [30, 98.4252]
+ ];
+ testCases.forEach(([metres, feet]) => {
+ test(`should convert ${metres} metres to ${feet} feet`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.metresToFeet(metres);
+
+ // Assert
+ expect(result).toEqual(feet);
+ });
+ });
+ });
+
+ describe('feetToMetres', () => {
+ const testCases = [
+ [1, 0.3048],
+ [10, 3.048],
+ [20, 6.096],
+ [30, 9.144]
+ ];
+ testCases.forEach(([feet, metres]) => {
+ test(`should convert ${feet} feet to ${metres} metres`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.feetToMetres(feet);
+
+ // Assert
+ expect(result).toEqual(metres);
+ });
+ });
+ });
+
+ describe('yardsToMetres', () => {
+ const testCases = [
+ [1, 0.9144],
+ [10, 9.14403],
+ [20, 18.28806],
+ [30, 27.43208]
+ ];
+ testCases.forEach(([yards, metres]) => {
+ test(`should convert ${yards} yards to ${metres} metres`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.yardsToMetres(yards);
+
+ // Assert
+ expect(result).toEqual(metres);
+ });
+ });
+ });
+
+ describe('kilometresToMiles', () => {
+ const testCases = [
+ [1, 0.62137],
+ [10, 6.21371],
+ [20, 12.42742],
+ [30, 18.64113]
+ ];
+ testCases.forEach(([km, miles]) => {
+ test(`should convert ${km} km to ${miles} miles`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.kilometresToMiles(km);
+
+ // Assert
+ expect(result).toEqual(miles);
+ });
+ });
+ });
+
+ describe('milesToKilometres', () => {
+ const testCases = [
+ [1, 1.60934],
+ [10, 16.09344],
+ [20, 32.18689],
+ [30, 48.28033]
+ ];
+ testCases.forEach(([miles, km]) => {
+ test(`should convert ${miles} miles to ${km} km`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.milesToKilometres(miles);
+
+ // Assert
+ expect(result).toEqual(km);
+ });
+ });
+ });
+
+ describe('centimetresToInches', () => {
+ const testCases = [
+ [1, 0.3937],
+ [10, 3.93701],
+ [20, 7.87402],
+ [30, 11.81103]
+ ];
+ testCases.forEach(([cm, inches]) => {
+ test(`should convert ${cm} cm to ${inches} inches`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.centimetresToInches(cm);
+
+ // Assert
+ expect(result).toEqual(inches);
+ });
+ });
+ });
+
+ describe('centimetresToFeet', () => {
+ const testCases = [
+ [1, 0.03281],
+ [10, 0.32808],
+ [20, 0.65617],
+ [30, 0.98425]
+ ];
+ testCases.forEach(([cm, feet]) => {
+ test(`should convert ${cm} cm to ${feet} feet`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.centimetresToFeet(cm);
+
+ // Assert
+ expect(result).toEqual(feet);
+ });
+ });
+ });
+
+ describe('feetToCentimetres', () => {
+ const testCases = [
+ [1, 30.48],
+ [10, 304.79999],
+ [20, 609.59998],
+ [30, 914.39997]
+ ];
+ testCases.forEach(([feet, cm]) => {
+ test(`should convert ${feet} feet to ${cm} cm`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.feetToCentimetres(feet);
+
+ // Assert
+ expect(result).toEqual(cm);
+ });
+ });
+ });
+
+ describe('inchesToCentimetres', () => {
+ const testCases = [
+ [1, 2.54],
+ [10, 25.39999],
+ [20, 50.79997],
+ [30, 76.19996]
+ ];
+ testCases.forEach(([inches, cm]) => {
+ test(`should convert ${inches} inches to ${cm} cm`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.inchesToCentimetres(inches);
+
+ // Assert
+ expect(result).toEqual(cm);
+ });
+ });
+ });
+
+ describe('feetToInches', () => {
+ const testCases = [
+ [1, 12],
+ [10, 120],
+ [20, 240],
+ [30, 360]
+ ];
+ testCases.forEach(([feet, inches]) => {
+ test(`should convert ${feet} feet to ${inches} inches`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.feetToInches(feet);
+
+ // Assert
+ expect(result).toEqual(inches);
+ });
+ });
+ });
+
+ describe('inchesToFeet', () => {
+ const testCases = [
+ [1, 0.08333],
+ [10, 0.83333],
+ [20, 1.66667],
+ [30, 2.5]
+ ];
+ testCases.forEach(([inches, feet]) => {
+ test(`should convert ${inches} inches to ${feet} feet`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.inchesToFeet(inches);
+
+ // Assert
+ expect(result).toEqual(feet);
+ });
+ });
+ });
+
+ describe('centimetresToMetres', () => {
+ const testCases = [
+ [1, 0.01],
+ [10, 0.1],
+ [20, 0.2],
+ [30, 0.3]
+ ];
+ testCases.forEach(([cm, metres]) => {
+ test(`should convert ${cm} cm to ${metres} metres`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.centimetresToMetres(cm);
+
+ // Assert
+ expect(result).toEqual(metres);
+ });
+ });
+ });
+
+ describe('metresToCentimetres', () => {
+ const testCases = [
+ [1, 100],
+ [10, 1000],
+ [20, 2000],
+ [30, 3000]
+ ];
+ testCases.forEach(([metres, cm]) => {
+ test(`should convert ${metres} metres to ${cm} cm`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.metresToCentimetres(metres);
+
+ // Assert
+ expect(result).toEqual(cm);
+ });
+ });
+ });
+
+ describe('kilometresToMetres', () => {
+ const testCases = [
+ [1, 1000],
+ [10, 10000],
+ [20, 20000],
+ [30, 30000]
+ ];
+ testCases.forEach(([km, metres]) => {
+ test(`should convert ${km} km to ${metres} metres`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.kilometresToMetres(km);
+
+ // Assert
+ expect(result).toEqual(metres);
+ });
+ });
+ });
+
+ describe('metresToKilometres', () => {
+ const testCases = [
+ [1, 0.001],
+ [10, 0.01],
+ [20, 0.02],
+ [30, 0.03]
+ ];
+ testCases.forEach(([metres, km]) => {
+ test(`should convert ${metres} metres to ${km} km`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.metresToKilometres(metres);
+
+ // Assert
+ expect(result).toEqual(km);
+ });
+ });
+ });
+
+ describe('kilometresToCentimetres', () => {
+ const testCases = [
+ [1, 100000],
+ [10, 1000000],
+ [20, 2000000],
+ [30, 3000000]
+ ];
+ testCases.forEach(([km, cm]) => {
+ test(`should convert ${km} km to ${cm} cm`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.kilometresToCentimetres(km);
+
+ // Assert
+ expect(result).toEqual(cm);
+ });
+ });
+ });
+
+ describe('centimetresToKilometres', () => {
+ const testCases = [
+ [1, 0.00001],
+ [10, 0.0001],
+ [20, 0.0002],
+ [30, 0.0003]
+ ];
+ testCases.forEach(([cm, km]) => {
+ test(`should convert ${cm} cm to ${km} km`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.centimetresToKilometres(cm);
+
+ // Assert
+ expect(result).toEqual(km);
+ });
+ });
+ });
+
+ describe('metresToInches', () => {
+ const testCases = [
+ [1, 39.3701],
+ [10, 393.701],
+ [20, 787.402],
+ [30, 1181.103]
+ ];
+ testCases.forEach(([metres, inches]) => {
+ test(`should convert ${metres} metres to ${inches} inches`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.metresToInches(metres);
+
+ // Assert
+ expect(result).toEqual(inches);
+ });
+ });
+ });
+
+ describe('inchesToMetres', () => {
+ const testCases = [
+ [1, 0.0254],
+ [10, 0.254],
+ [20, 0.508],
+ [30, 0.762]
+ ];
+ testCases.forEach(([inches, metres]) => {
+ test(`should convert ${inches} inches to ${metres} metres`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.inchesToMetres(inches);
+
+ // Assert
+ expect(result).toEqual(metres);
+ });
+ });
+ });
+
+ describe('kilometresToYards', () => {
+ const testCases = [
+ [1, 1093.61],
+ [10, 10936.1],
+ [20, 21872.2],
+ [30, 32808.3]
+ ];
+ testCases.forEach(([km, yards]) => {
+ test(`should convert ${km} km to ${yards} yards`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.kilometresToYards(km);
+
+ // Assert
+ expect(result).toEqual(yards);
+ });
+ });
+ });
+
+ describe('yardsToKilometres', () => {
+ const testCases = [
+ [1, 0.00091],
+ [10, 0.00914],
+ [20, 0.01829],
+ [30, 0.02743]
+ ];
+ testCases.forEach(([yards, km]) => {
+ test(`should convert ${yards} yards to ${km} km`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.yardsToKilometres(yards);
+
+ // Assert
+ expect(result).toEqual(km);
+ });
+ });
+ });
+
+ describe('milesToYards', () => {
+ const testCases = [
+ [1, 1760],
+ [10, 17600],
+ [20, 35200],
+ [30, 52800]
+ ];
+ testCases.forEach(([miles, yards]) => {
+ test(`should convert ${miles} miles to ${yards} yards`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.milesToYards(miles);
+
+ // Assert
+ expect(result).toEqual(yards);
+ });
+ });
+ });
+
+ describe('yardsToMiles', () => {
+ const testCases = [
+ [1, 0.00057],
+ [10, 0.00568],
+ [20, 0.01136],
+ [30, 0.01705]
+ ];
+ testCases.forEach(([yards, miles]) => {
+ test(`should convert ${yards} yards to ${miles} miles`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.yardsToMiles(yards);
+
+ // Assert
+ expect(result).toEqual(miles);
+ });
+ });
+ });
+
+ describe('milesToFeet', () => {
+ const testCases = [
+ [1, 5280],
+ [10, 52800],
+ [20, 105600],
+ [30, 158400]
+ ];
+ testCases.forEach(([miles, feet]) => {
+ test(`should convert ${miles} miles to ${feet} feet`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.milesToFeet(miles);
+
+ // Assert
+ expect(result).toEqual(feet);
+ });
+ });
+ });
+
+ describe('feetToMiles', () => {
+ const testCases = [
+ [1, 0.00019],
+ [10, 0.00189],
+ [20, 0.00379],
+ [30, 0.00568]
+ ];
+ testCases.forEach(([feet, miles]) => {
+ test(`should convert ${feet} feet to ${miles} miles`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.feetToMiles(feet);
+
+ // Assert
+ expect(result).toEqual(miles);
+ });
+ });
+ });
+
+ describe('milesToInches', () => {
+ const testCases = [
+ [1, 63360],
+ [10, 633600],
+ [20, 1267200],
+ [30, 1900800]
+ ];
+ testCases.forEach(([miles, inches]) => {
+ test(`should convert ${miles} miles to ${inches} inches`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.milesToInches(miles);
+
+ // Assert
+ expect(result).toEqual(inches);
+ });
+ });
+ });
+
+ describe('inchesToMiles', () => {
+ const testCases = [
+ [1, 0.00002],
+ [10, 0.00016],
+ [20, 0.00032],
+ [30, 0.00047]
+ ];
+ testCases.forEach(([inches, miles]) => {
+ test(`should convert ${inches} inches to ${miles} miles`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.inchesToMiles(inches);
+
+ // Assert
+ expect(result).toEqual(miles);
+ });
+ });
+ });
+
+ describe('yardsToFeet', () => {
+ const testCases = [
+ [1, 3],
+ [10, 30],
+ [20, 60],
+ [30, 90]
+ ];
+ testCases.forEach(([yards, feet]) => {
+ test(`should convert ${yards} yards to ${feet} feet`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.yardsToFeet(yards);
+
+ // Assert
+ expect(result).toEqual(feet);
+ });
+ });
+ });
+
+ describe('feetToYards', () => {
+ const testCases = [
+ [1, 0.33333],
+ [10, 3.33333],
+ [20, 6.66666],
+ [30, 9.99999]
+ ];
+ testCases.forEach(([feet, yards]) => {
+ test(`should convert ${feet} feet to ${yards} yards`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.feetToYards(feet);
+
+ // Assert
+ expect(result).toEqual(yards);
+ });
+ });
+ });
+
+ describe('milesToMetres', () => {
+ const testCases = [
+ [1, 1609.34],
+ [10, 16093.44],
+ [20, 32186.89],
+ [30, 48280.33]
+ ];
+ testCases.forEach(([miles, metres]) => {
+ test(`should convert ${miles} miles to ${metres} metres`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.milesToMetres(miles);
+
+ // Assert
+ expect(result).toEqual(metres);
+ });
+ });
+ });
+
+ describe('metresToMiles', () => {
+ const testCases = [
+ [1, 0.00062],
+ [10, 0.00621],
+ [20, 0.01243],
+ [30, 0.01864]
+ ];
+ testCases.forEach(([metres, miles]) => {
+ test(`should convert ${metres} metres to ${miles} miles`, () => {
+ // Arrange & Act
+ const result = DistanceConverter.metresToMiles(metres);
+
+ // Assert
+ expect(result).toEqual(miles);
+ });
+ });
+ });
+});
diff --git a/src/units/distance-converter.ts b/src/units/distance-converter.ts
new file mode 100644
index 0000000..c97ba25
--- /dev/null
+++ b/src/units/distance-converter.ts
@@ -0,0 +1,182 @@
+import { Decimal } from 'decimal.js';
+
+export class DistanceConverter {
+ private static readonly METRES_TO_YARDS = new Decimal(1.09361);
+ private static readonly KILOMETRES_TO_MILES = new Decimal(0.621371);
+ private static readonly CENTIMETRES_TO_INCHES = new Decimal(0.393701);
+ private static readonly METRES_TO_FEET = new Decimal(3.28084);
+ private static readonly CENTIMETRES_TO_FEET = new Decimal(0.0328084);
+ private static readonly FEET_TO_INCHES = new Decimal(12);
+ private static readonly METRES_TO_INCHES = new Decimal(39.3701);
+ private static readonly INCHES_TO_METRES = new Decimal(0.0254);
+ private static readonly KILOMETRES_TO_YARDS = new Decimal(1093.61);
+ private static readonly YARDS_TO_KILOMETRES = new Decimal(0.0009144);
+ private static readonly MILES_TO_YARDS = new Decimal(1760);
+ private static readonly YARDS_TO_MILES = new Decimal(0.000568182);
+ private static readonly MILES_TO_FEET = new Decimal(5280);
+ private static readonly FEET_TO_MILES = new Decimal(0.000189394);
+ private static readonly MILES_TO_INCHES = new Decimal(63360);
+ private static readonly INCHES_TO_MILES = new Decimal(0.0000157828);
+ private static readonly YARDS_TO_FEET = new Decimal(3);
+ private static readonly FEET_TO_YARDS = new Decimal(0.333333);
+
+ static metresToYards(metres: number): number {
+ const result = new Decimal(metres).mul(this.METRES_TO_YARDS);
+ return Number(result.toFixed(5));
+ }
+
+ static metresToFeet(metres: number): number {
+ const result = new Decimal(metres).mul(this.METRES_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+
+ static feetToMetres(feet: number): number {
+ const result = new Decimal(feet).div(this.METRES_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+
+ static yardsToMetres(yards: number): number {
+ const result = new Decimal(yards).div(this.METRES_TO_YARDS);
+ return Number(result.toFixed(5));
+ }
+
+ static kilometresToMiles(kilometres: number): number {
+ const result = new Decimal(kilometres).mul(this.KILOMETRES_TO_MILES);
+ return Number(result.toFixed(5));
+ }
+
+ static milesToKilometres(miles: number): number {
+ const result = new Decimal(miles).div(this.KILOMETRES_TO_MILES);
+ return Number(result.toFixed(5));
+ }
+
+ static centimetresToInches(centimetres: number): number {
+ const result = new Decimal(centimetres).mul(this.CENTIMETRES_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+
+ static centimetresToFeet(centimetres: number): number {
+ const result = new Decimal(centimetres).mul(this.CENTIMETRES_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+
+ static feetToCentimetres(feet: number): number {
+ const result = new Decimal(feet).div(this.CENTIMETRES_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+
+ static inchesToCentimetres(inches: number): number {
+ const result = new Decimal(inches).div(this.CENTIMETRES_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+
+ static feetToInches(feet: number): number {
+ const result = new Decimal(feet).mul(this.FEET_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+
+ static inchesToFeet(inches: number): number {
+ const result = new Decimal(inches).div(this.FEET_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+
+ static centimetresToMetres(centimetres: number): number {
+ const result = new Decimal(centimetres).div(100);
+ return Number(result.toFixed(5));
+ }
+
+ static metresToCentimetres(metres: number): number {
+ const result = new Decimal(metres).mul(100);
+ return Number(result.toFixed(5));
+ }
+
+ static kilometresToMetres(kilometres: number): number {
+ const result = new Decimal(kilometres).mul(1000);
+ return Number(result.toFixed(5));
+ }
+
+ static metresToKilometres(metres: number): number {
+ const result = new Decimal(metres).div(1000);
+ return Number(result.toFixed(5));
+ }
+
+ static kilometresToCentimetres(kilometres: number): number {
+ const result = new Decimal(kilometres).mul(100000);
+ return Number(result.toFixed(5));
+ }
+
+ static centimetresToKilometres(centimetres: number): number {
+ const result = new Decimal(centimetres).div(100000);
+ return Number(result.toFixed(5));
+ }
+
+ static metresToInches(metres: number): number {
+ const result = new Decimal(metres).mul(this.METRES_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+
+ static inchesToMetres(inches: number): number {
+ const result = new Decimal(inches).mul(this.INCHES_TO_METRES);
+ return Number(result.toFixed(5));
+ }
+
+ static kilometresToYards(kilometres: number): number {
+ const result = new Decimal(kilometres).mul(this.KILOMETRES_TO_YARDS);
+ return Number(result.toFixed(5));
+ }
+
+ static yardsToKilometres(yards: number): number {
+ const result = new Decimal(yards).mul(this.YARDS_TO_KILOMETRES);
+ return Number(result.toFixed(5));
+ }
+
+ static milesToYards(miles: number): number {
+ const result = new Decimal(miles).mul(this.MILES_TO_YARDS);
+ return Number(result.toFixed(5));
+ }
+
+ static yardsToMiles(yards: number): number {
+ const result = new Decimal(yards).mul(this.YARDS_TO_MILES);
+ return Number(result.toFixed(5));
+ }
+
+ static milesToFeet(miles: number): number {
+ const result = new Decimal(miles).mul(this.MILES_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+
+ static feetToMiles(feet: number): number {
+ const result = new Decimal(feet).mul(this.FEET_TO_MILES);
+ return Number(result.toFixed(5));
+ }
+
+ static milesToInches(miles: number): number {
+ const result = new Decimal(miles).mul(this.MILES_TO_INCHES);
+ return Number(result.toFixed(5));
+ }
+
+ static inchesToMiles(inches: number): number {
+ const result = new Decimal(inches).mul(this.INCHES_TO_MILES);
+ return Number(result.toFixed(5));
+ }
+
+ static yardsToFeet(yards: number): number {
+ const result = new Decimal(yards).mul(this.YARDS_TO_FEET);
+ return Number(result.toFixed(5));
+ }
+
+ static feetToYards(feet: number): number {
+ const result = new Decimal(feet).mul(this.FEET_TO_YARDS);
+ return Number(result.toFixed(5));
+ }
+
+ static milesToMetres(miles: number): number {
+ const kilometres = this.milesToKilometres(miles);
+ return this.kilometresToMetres(kilometres);
+ }
+
+ static metresToMiles(metres: number): number {
+ const kilometres = this.metresToKilometres(metres);
+ return this.kilometresToMiles(kilometres);
+ }
+}
diff --git a/src/units/mass-converter.test.ts b/src/units/mass-converter.test.ts
new file mode 100644
index 0000000..20d8fa6
--- /dev/null
+++ b/src/units/mass-converter.test.ts
@@ -0,0 +1,220 @@
+import { MassConverter } from './mass-converter';
+import { describe, expect, test } from 'bun:test';
+
+describe('MassConverter', () => {
+ describe('kilogramsToPounds', () => {
+ const testCases = [
+ [1, 2.20462],
+ [10, 22.0462],
+ [20, 44.0924],
+ [30, 66.1386]
+ ];
+ testCases.forEach(([kilos, pounds]) => {
+ test(`should convert ${kilos} kg to ${pounds} lbs`, () => {
+ // Arrange & Act
+ const result = MassConverter.kilogramsToPounds(kilos);
+
+ // Assert
+ expect(result).toEqual(pounds);
+ });
+ });
+ });
+
+ describe('poundsToKilograms', () => {
+ const testCases = [
+ [1, 0.45359],
+ [10, 4.53593],
+ [20, 9.07186],
+ [30, 13.60779]
+ ];
+ testCases.forEach(([lbs, kg]) => {
+ test(`should convert ${lbs} lbs to ${kg} kg`, () => {
+ // Arrange & Act
+ const result = MassConverter.poundsToKilograms(lbs);
+
+ // Assert
+ expect(result).toEqual(kg);
+ });
+ });
+ });
+
+ describe('gramsToKilograms', () => {
+ const testCases = [
+ [1, 0.001],
+ [10, 0.01],
+ [20, 0.02],
+ [30, 0.03]
+ ];
+ testCases.forEach(([g, kg]) => {
+ test(`should convert ${g} g to ${kg} kg`, () => {
+ // Arrange & Act
+ const result = MassConverter.gramsToKilograms(g);
+
+ // Assert
+ expect(result).toEqual(kg);
+ });
+ });
+ });
+
+ describe('kilogramsToGrams', () => {
+ const testCases = [
+ [1, 1000],
+ [10, 10000],
+ [20, 20000],
+ [30, 30000]
+ ];
+ testCases.forEach(([kilos, g]) => {
+ test(`should convert ${kilos} kg to ${g} g`, () => {
+ // Arrange & Act
+ const result = MassConverter.kilogramsToGrams(kilos);
+
+ // Assert
+ expect(result).toEqual(g);
+ });
+ });
+ });
+
+ describe('gramsToOunces', () => {
+ const testCases = [
+ [1, 0.03527],
+ [10, 0.35274],
+ [20, 0.70548],
+ [30, 1.05822]
+ ];
+ testCases.forEach(([g, oz]) => {
+ test(`should convert ${g} g to ${oz} oz`, () => {
+ // Arrange & Act
+ const result = MassConverter.gramsToOunces(g);
+
+ // Assert
+ expect(result).toEqual(oz);
+ });
+ });
+ });
+
+ describe('ouncesToGrams', () => {
+ const testCases = [
+ [1, 28.34949],
+ [10, 283.49493],
+ [20, 566.98985],
+ [30, 850.48478]
+ ];
+ testCases.forEach(([oz, g]) => {
+ test(`should convert ${oz} oz to ${g} g`, () => {
+ // Arrange & Act
+ const result = MassConverter.ouncesToGrams(oz);
+
+ // Assert
+ expect(result).toEqual(g);
+ });
+ });
+ });
+
+ describe('ouncesToPounds', () => {
+ const testCases = [
+ [1, 0.0625],
+ [10, 0.625],
+ [20, 1.25],
+ [30, 1.875]
+ ];
+ testCases.forEach(([oz, lbs]) => {
+ test(`should convert ${oz} oz to ${lbs} lbs`, () => {
+ // Arrange & Act
+ const result = MassConverter.ouncesToPounds(oz);
+
+ // Assert
+ expect(result).toEqual(lbs);
+ });
+ });
+ });
+
+ describe('poundsToOunces', () => {
+ const testCases = [
+ [1, 16],
+ [10, 160],
+ [20, 320],
+ [30, 480]
+ ];
+ testCases.forEach(([lbs, oz]) => {
+ test(`should convert ${lbs} lbs to ${oz} oz`, () => {
+ // Arrange & Act
+ const result = MassConverter.poundsToOunces(lbs);
+
+ // Assert
+ expect(result).toEqual(oz);
+ });
+ });
+ });
+
+ describe('gramsToPounds', () => {
+ const testCases = [
+ [1, 0.0022],
+ [10, 0.02205],
+ [20, 0.04409],
+ [30, 0.06614]
+ ];
+ testCases.forEach(([g, lbs]) => {
+ test(`should convert ${g} g to ${lbs} lbs`, () => {
+ // Arrange & Act
+ const result = MassConverter.gramsToPounds(g);
+
+ // Assert
+ expect(result).toEqual(lbs);
+ });
+ });
+ });
+
+ describe('poundsToGrams', () => {
+ const testCases = [
+ [1, 453.59291],
+ [10, 4535.92909],
+ [20, 9071.85819],
+ [30, 13607.78728]
+ ];
+ testCases.forEach(([lbs, g]) => {
+ test(`should convert ${lbs} lbs to ${g} g`, () => {
+ // Arrange & Act
+ const result = MassConverter.poundsToGrams(lbs);
+
+ // Assert
+ expect(result).toEqual(g);
+ });
+ });
+ });
+
+ describe('kilogramsToOunces', () => {
+ const testCases = [
+ [1, 35.274],
+ [10, 352.74],
+ [20, 705.48],
+ [30, 1058.22]
+ ];
+ testCases.forEach(([kilos, oz]) => {
+ test(`should convert ${kilos} kg to ${oz} oz`, () => {
+ // Arrange & Act
+ const result = MassConverter.kilogramsToOunces(kilos);
+
+ // Assert
+ expect(result).toEqual(oz);
+ });
+ });
+ });
+
+ describe('ouncesToKilograms', () => {
+ const testCases = [
+ [1, 0.02835],
+ [10, 0.28349],
+ [20, 0.56699],
+ [30, 0.85048]
+ ];
+ testCases.forEach(([oz, kg]) => {
+ test(`should convert ${oz} oz to ${kg} kg`, () => {
+ // Arrange & Act
+ const result = MassConverter.ouncesToKilograms(oz);
+
+ // Assert
+ expect(result).toEqual(kg);
+ });
+ });
+ });
+});
diff --git a/src/units/mass-converter.ts b/src/units/mass-converter.ts
new file mode 100644
index 0000000..160dc4b
--- /dev/null
+++ b/src/units/mass-converter.ts
@@ -0,0 +1,68 @@
+
+import { Decimal } from 'decimal.js';
+
+export class MassConverter {
+ private static readonly GRAMS_TO_OUNCES = new Decimal(0.035274);
+ private static readonly KILOGRAMS_TO_POUNDS = new Decimal(2.20462);
+ private static readonly KILOGRAMS_TO_OUNCES = new Decimal(35.274);
+
+ static kilogramsToPounds(kilos: number): number {
+ const result = new Decimal(kilos).mul(this.KILOGRAMS_TO_POUNDS);
+ return Number(result.toFixed(5));
+ }
+
+ static poundsToKilograms(pounds: number): number {
+ const result = new Decimal(pounds).div(this.KILOGRAMS_TO_POUNDS);
+ return Number(result.toFixed(5));
+ }
+
+ static gramsToKilograms(grams: number): number {
+ const result = new Decimal(grams).div(1000);
+ return Number(result.toFixed(5));
+ }
+
+ static kilogramsToGrams(kilos: number): number {
+ const result = new Decimal(kilos).mul(1000);
+ return Number(result.toFixed(5));
+ }
+
+ static gramsToOunces(grams: number): number {
+ const result = new Decimal(grams).mul(this.GRAMS_TO_OUNCES);
+ return Number(result.toFixed(5));
+ }
+
+ static ouncesToGrams(ounces: number): number {
+ const result = new Decimal(ounces).div(this.GRAMS_TO_OUNCES);
+ return Number(result.toFixed(5));
+ }
+
+ static ouncesToPounds(ounces: number): number {
+ const result = new Decimal(ounces).div(16);
+ return Number(result.toFixed(5));
+ }
+
+ static poundsToOunces(pounds: number): number {
+ const result = new Decimal(pounds).mul(16);
+ return Number(result.toFixed(5));
+ }
+
+ static gramsToPounds(grams: number): number {
+ const result = new Decimal(grams).mul(this.KILOGRAMS_TO_POUNDS).div(1000);
+ return Number(result.toFixed(5));
+ }
+
+ static poundsToGrams(pounds: number): number {
+ const result = new Decimal(pounds).div(this.KILOGRAMS_TO_POUNDS).mul(1000);
+ return Number(result.toFixed(5));
+ }
+
+ static kilogramsToOunces(kilos: number): number {
+ const result = new Decimal(kilos).mul(this.KILOGRAMS_TO_OUNCES);
+ return Number(result.toFixed(5));
+ }
+
+ static ouncesToKilograms(ounces: number): number {
+ const result = new Decimal(ounces).div(this.KILOGRAMS_TO_OUNCES);
+ return Number(result.toFixed(5));
+ }
+}
diff --git a/src/units/model.ts b/src/units/model.ts
new file mode 100644
index 0000000..60aebdb
--- /dev/null
+++ b/src/units/model.ts
@@ -0,0 +1,60 @@
+export const MASS_UNITS = {
+ GRAM: { long: 'gram', short: 'g' },
+ KILOGRAM: { long: 'kilogram', short: 'kg' },
+ OUNCE: { long: 'ounce', short: 'oz' },
+ POUND: { long: 'pound', short: 'lb' },
+} as const;
+
+export const TEMPERATURE_UNITS = {
+ CELSIUS: { long: 'celsius', short: 'c' },
+ CELSIUS_FAN_OVEN: { long: 'celsius (fan oven)', short: 'c (fan)' },
+ FAHRENHEIT: { long: 'fahrenheit', short: 'f' }
+};
+
+export const VOLUME_UNITS = {
+ MILLILITRE: { long: 'millilitre', short: 'ml' },
+ FLUID_OUNCE: { long: 'fluid ounce', short: 'fl oz' },
+ LITRE: { long: 'litre', short: 'l' },
+ GALLON: { long: 'gallon', short: 'gal' },
+ PINT: { long: 'pint', short: 'pt' },
+} as const;
+
+export const DISTANCE_UNITS = {
+ CENTIMETRE: { long: 'centimetre', short: 'cm' },
+ METRE: { long: 'metre', short: 'm' },
+ KILOMETRE: { long: 'kilometre', short: 'km' },
+ INCH: { long: 'inch', short: 'in' },
+ FOOT: { long: 'foot', short: 'ft' },
+ YARD: { long: 'yard', short: 'yd' },
+ MILE: { long: 'mile', short: 'mi' },
+}
+
+type MassUnitShort = typeof MASS_UNITS[keyof typeof MASS_UNITS]['short'];
+type TemperatureUnitShort = typeof TEMPERATURE_UNITS[keyof typeof TEMPERATURE_UNITS]['short'];
+type VolumeUnitShort = typeof VOLUME_UNITS[keyof typeof VOLUME_UNITS]['short'];
+type DistanceUnitShort = typeof DISTANCE_UNITS[keyof typeof DISTANCE_UNITS]['short'];
+
+export type UnitShort = MassUnitShort | TemperatureUnitShort | VolumeUnitShort | DistanceUnitShort;
+
+export function isMassUnit(unit: UnitShort): boolean {
+ return Object.values(MASS_UNITS).some((u) => u.short === unit);
+}
+
+export function isTemperatureUnit(unit: UnitShort): boolean {
+ return Object.values(TEMPERATURE_UNITS).some((u) => u.short === unit);
+}
+
+export function isVolumeUnit(unit: UnitShort): boolean {
+ return Object.values(VOLUME_UNITS).some((u) => u.short === unit);
+}
+
+export function isDistanceUnit(unit: UnitShort): boolean {
+ return Object.values(DISTANCE_UNITS).some((u) => u.short === unit);
+}
+
+export const unitMap = {
+ ...MASS_UNITS,
+ ...TEMPERATURE_UNITS,
+ ...VOLUME_UNITS,
+ ...DISTANCE_UNITS
+};
diff --git a/src/units/temperatre-converter.ts b/src/units/temperatre-converter.ts
new file mode 100644
index 0000000..fa2dba4
--- /dev/null
+++ b/src/units/temperatre-converter.ts
@@ -0,0 +1,46 @@
+import { Decimal } from 'decimal.js';
+
+export class TemperatureConverter {
+ private static readonly CELSIUS_TO_FAHRENHEIT = 1.8;
+ private static readonly FAHRENHEIT_TO_CELSIUS = 0.5555555555555556;
+
+ static celsiusToFahrenheit(celsius: number): number {
+ return (celsius * this.CELSIUS_TO_FAHRENHEIT) + 32;
+ }
+
+ static fahrenheitToCelsius(fahrenheit: number): number {
+ return (fahrenheit - 32) * this.FAHRENHEIT_TO_CELSIUS;
+ }
+
+ /**
+ * American ovens don't tend to be fanned, whereas british ones are,
+ * requiring the temperature be adjusted by 10%.
+ */
+ static celsiusFanOvenToFahrenheit(celsiusFanOven: number): number {
+ const celsius = new Decimal(celsiusFanOven).div(0.9);
+
+ return this.celsiusToFahrenheit(celsius.toNumber());
+ }
+
+ /**
+ * American ovens don't tend to be fanned, whereas british ones are,
+ * requiring the temperature be adjusted by 10%.
+ */
+ static fahrenheitToCelsiusFanOven(fahrenheit: number): number {
+ const celsius = new Decimal(this.fahrenheitToCelsius(fahrenheit));
+
+ return celsius.mul(0.9).toNumber();
+ }
+
+ static celsiusToCelsiusFanOven(celsius: number): number {
+ const c = new Decimal(celsius);
+
+ return c.mul(0.9).toNumber();
+ }
+
+ static celsiusFanOvenToCelsius(celsiusFanOven: number): number {
+ const c = new Decimal(celsiusFanOven);
+
+ return c.div(0.9).toNumber();
+ }
+}
diff --git a/src/units/temperature-converter.test.ts b/src/units/temperature-converter.test.ts
new file mode 100644
index 0000000..dd49262
--- /dev/null
+++ b/src/units/temperature-converter.test.ts
@@ -0,0 +1,118 @@
+import { describe, expect, test } from 'bun:test';
+import { TemperatureConverter } from "./temperatre-converter";
+
+describe('TemperatureConverter', () => {
+ describe('celsiusToFahrenheit', () => {
+ const testCases = [
+ [0, 32],
+ [10, 50],
+ [15, 59],
+ [20, 68],
+ [30, 86]
+ ];
+ testCases.forEach(([c, f]) => {
+ test(`should convert ${c} C to ${f} F`, () => {
+ // Arrange & Act
+ const result = TemperatureConverter.celsiusToFahrenheit(c);
+
+ // Assert
+ expect(result).toEqual(f);
+ });
+ });
+ });
+
+ describe('fahrenheitToCelsius', () => {
+ const testCases = [
+ [32, 0],
+ [50, 10],
+ [59, 15],
+ [68, 20],
+ [86, 30]
+ ];
+ testCases.forEach(([f, c]) => {
+ test(`should convert ${f} F to ${c} C`, () => {
+ // Arrange & Act
+ const result = TemperatureConverter.fahrenheitToCelsius(f);
+
+ // Assert
+ expect(result).toEqual(c);
+ });
+ });
+ });
+
+ describe('celsiusFanOvenToFahrenheit', () => {
+ const testCases = [
+ [0, 32],
+ [10, 52],
+ [15, 62],
+ [20, 72],
+ [30, 92]
+ ];
+ testCases.forEach(([c, f]) => {
+ test(`should convert ${c} C (fan oven) to ${f} F`, () => {
+ // Arrange & Act
+ const result = TemperatureConverter.celsiusFanOvenToFahrenheit(c);
+
+ // Assert
+ expect(result).toEqual(f);
+ });
+ });
+ });
+
+ describe('fahrenheitToCelsiusFanOven', () => {
+ const testCases = [
+ [32, 0],
+ [50, 9],
+ [59, 13.5],
+ [68, 18],
+ [86, 27]
+ ];
+ testCases.forEach(([f, c]) => {
+ test(`should convert ${f} F to ${c} C (fan oven)`, () => {
+ // Arrange & Act
+ const result = TemperatureConverter.fahrenheitToCelsiusFanOven(f);
+
+ // Assert
+ expect(result).toEqual(c);
+ });
+ });
+ });
+
+ describe('celsiusToCelsiusFanOven', () => {
+ const testCases = [
+ [0, 0],
+ [10, 9],
+ [15, 13.5],
+ [20, 18],
+ [30, 27]
+ ];
+ testCases.forEach(([c, cf]) => {
+ test(`should convert ${c} C to ${cf} C (fan oven)`, () => {
+ // Arrange & Act
+ const result = TemperatureConverter.celsiusToCelsiusFanOven(c);
+
+ // Assert
+ expect(result).toEqual(cf);
+ });
+ });
+ });
+
+ describe('celsiusFanOvenToCelsius', () => {
+ const testCases = [
+ [0, 0],
+ [9, 10],
+ [13.5, 15],
+ [18, 20],
+ [27, 30]
+ ];
+ testCases.forEach(([cf, c]) => {
+ test(`should convert ${cf} C (fan oven) to ${c} C`, () => {
+ // Arrange & Act
+ const result = TemperatureConverter.celsiusFanOvenToCelsius(cf);
+
+ // Assert
+ expect(result).toEqual(c);
+ });
+ });
+ });
+});
diff --git a/src/units/volume-converter.test.ts b/src/units/volume-converter.test.ts
new file mode 100644
index 0000000..0068ad4
--- /dev/null
+++ b/src/units/volume-converter.test.ts
@@ -0,0 +1,293 @@
+import { describe, expect, test } from 'bun:test';
+import { VolumeConverter } from './volume-converter';
+
+describe('VolumeConverter', () => {
+ describe('litresToPints', () => {
+ const testCases = [
+ [1, 1.75975],
+ [10, 17.5975],
+ [20, 35.195],
+ [30, 52.7925]
+ ];
+ testCases.forEach(([litres, pints]) => {
+ test(`should convert ${litres} l to ${pints} pt`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.litresToPints(litres);
+
+ // Assert
+ expect(result).toEqual(pints);
+ });
+ });
+ });
+
+ describe('pintsToLitres', () => {
+ const testCases = [
+ [1, 0.56826],
+ [10, 5.68261],
+ [20, 11.36522],
+ [30, 17.04783]
+ ];
+ testCases.forEach(([pints, litres]) => {
+ test(`should convert ${pints} pt to ${litres} l`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.pintsToLitres(pints);
+
+ // Assert
+ expect(result).toEqual(litres);
+ });
+ });
+ });
+
+ describe('litresToFluidOunces', () => {
+ const testCases = [
+ [1, 33.814],
+ [10, 338.14],
+ [20, 676.28],
+ [30, 1014.42]
+ ];
+ testCases.forEach(([litres, fluidOunces]) => {
+ test(`should convert ${litres} l to ${fluidOunces} fl oz`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.litresToFluidOunces(litres);
+
+ // Assert
+ expect(result).toEqual(fluidOunces);
+ });
+ });
+ });
+
+ describe('fluidOuncesToLitres', () => {
+ const testCases = [
+ [1, 0.02957],
+ [10, 0.29574],
+ [20, 0.59147],
+ [30, 0.88721]
+ ];
+ testCases.forEach(([fluidOunces, litres]) => {
+ test(`should convert ${fluidOunces} fl oz to ${litres} l`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.fluidOuncesToLitres(fluidOunces);
+
+ // Assert
+ expect(result).toEqual(litres);
+ });
+ });
+ });
+
+ describe('gallonsToPints', () => {
+ const testCases = [
+ [1, 8],
+ [2, 16],
+ [3, 24],
+ [4, 32]
+ ];
+ testCases.forEach(([gallons, pints]) => {
+ test(`should convert ${gallons} gal to ${pints} pt`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.gallonsToPints(gallons);
+
+ // Assert
+ expect(result).toEqual(pints);
+ });
+ });
+ });
+
+ describe('pintsToGallons', () => {
+ const testCases = [
+ [1, 0.125],
+ [2, 0.25],
+ [3, 0.375],
+ [4, 0.5]
+ ];
+ testCases.forEach(([pints, gallons]) => {
+ test(`should convert ${pints} pt to ${gallons} gal`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.pintsToGallons(pints);
+
+ // Assert
+ expect(result).toEqual(gallons);
+ });
+ });
+ });
+
+ describe('gallonsToMillilitres', () => {
+ const testCases = [
+ [1, 3785.41],
+ [2, 7570.82],
+ [3, 11356.23],
+ [4, 15141.64]
+ ];
+ testCases.forEach(([gallons, millilitres]) => {
+ test(`should convert ${gallons} gal to ${millilitres} ml`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.gallonsToMillilitres(gallons);
+
+ // Assert
+ expect(result).toEqual(millilitres);
+ });
+ });
+ });
+
+ describe('millilitresToGallons', () => {
+ const testCases = [
+ [1000, 0.26417],
+ [2000, 0.52834],
+ [3000, 0.79252],
+ [4000, 1.05669]
+ ];
+ testCases.forEach(([millilitres, gallons]) => {
+ test(`should convert ${millilitres} ml to ${gallons} gal`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.millilitresToGallons(millilitres);
+
+ // Assert
+ expect(result).toEqual(gallons);
+ });
+ });
+ });
+
+ describe('pintsToFluidOunces', () => {
+ const testCases = [
+ [1, 20],
+ [2, 40],
+ [3, 60],
+ [4, 80]
+ ];
+ testCases.forEach(([pints, fluidOunces]) => {
+ test(`should convert ${pints} pt to ${fluidOunces} fl oz`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.pintsToFluidOunces(pints);
+
+ // Assert
+ expect(result).toEqual(fluidOunces);
+ });
+ });
+ });
+
+ describe('fluidOuncesToPints', () => {
+ const testCases = [
+ [1, 0.05],
+ [2, 0.1],
+ [3, 0.15],
+ [4, 0.2]
+ ];
+ testCases.forEach(([fluidOunces, pints]) => {
+ test(`should convert ${fluidOunces} fl oz to ${pints} pt`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.fluidOuncesToPints(fluidOunces);
+
+ // Assert
+ expect(result).toEqual(pints);
+ });
+ });
+ });
+
+ describe('millilitresToFluidOunces', () => {
+ const testCases = [
+ [1, .03381],
+ [10, .33814],
+ [20, .67628],
+ [30, 1.01442]
+ ];
+ testCases.forEach(([millilitres, fluidOunces]) => {
+ test(`should convert ${millilitres} ml to ${fluidOunces} fl oz`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.millilitresToFluidOunces(millilitres);
+
+ // Assert
+ expect(result).toEqual(fluidOunces);
+ });
+ });
+ });
+
+ describe('fluidOuncesToMillilitres', () => {
+ const testCases = [
+ [1, 29.57355],
+ [10, 295.73549],
+ [20, 591.47099],
+ [30, 887.20648]
+ ];
+ testCases.forEach(([fluidOunces, millilitres]) => {
+ test(`should convert ${fluidOunces} fl oz to ${millilitres} ml`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.fluidOuncesToMillilitres(fluidOunces);
+
+ // Assert
+ expect(result).toEqual(millilitres);
+ });
+ });
+ });
+
+ describe('litresToGallons', () => {
+ const testCases = [
+ [1, .26417],
+ [10, 2.64172],
+ [20, 5.28344],
+ [30, 7.92516]
+ ];
+ testCases.forEach(([litres, gallons]) => {
+ test(`should convert ${litres} l to ${gallons} gal`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.litresToGallons(litres);
+
+ // Assert
+ expect(result).toEqual(gallons);
+ });
+ });
+ });
+
+ describe('gallonsToLitres', () => {
+ const testCases = [
+ [1, 3.78541],
+ [10, 37.85413],
+ [20, 75.70825],
+ [30, 113.56238]
+ ];
+ testCases.forEach(([gallons, litres]) => {
+ test(`should convert ${gallons} gal to ${litres} l`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.gallonsToLitres(gallons);
+
+ // Assert
+ expect(result).toEqual(litres);
+ });
+ });
+ });
+
+ describe('millilitresToPints', () => {
+ const testCases = [
+ [10, .0176],
+ [20, .0352],
+ [30, .0528],
+ [100, .176],
+ [10000, 17.6],
+ ];
+ testCases.forEach(([millilitres, pints]) => {
+ test(`should convert ${millilitres} ml to ${pints} pt`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.millilitresToPints(millilitres);
+
+ // Assert
+ expect(result).toEqual(pints);
+ });
+ });
+ });
+
+ describe('pintsToMillilitres', () => {
+ const testCases = [
+ [1, 568.18182],
+ [2.5, 1420.45455],
+ [10, 5681.81818],
+ [20, 11363.63636],
+ ];
+ testCases.forEach(([pints, millilitres]) => {
+ test(`should convert ${pints} pt to ${millilitres} ml`, () => {
+ // Arrange & Act
+ const result = VolumeConverter.pintsToMillilitres(pints);
+
+ // Assert
+ expect(result).toEqual(millilitres);
+ });
+ });
+ });
+});
diff --git a/src/units/volume-converter.ts b/src/units/volume-converter.ts
new file mode 100644
index 0000000..d2039b1
--- /dev/null
+++ b/src/units/volume-converter.ts
@@ -0,0 +1,130 @@
+
+import { Decimal } from 'decimal.js';
+
+export class VolumeConverter {
+ private static readonly LITRES_TO_PINTS = new Decimal(1.75975);
+ private static readonly PINTS_TO_LITRES = new Decimal(0.568261);
+ private static readonly LITRES_TO_FLUID_OUNCES = new Decimal(33.814);
+ private static readonly FLUID_OUNCES_TO_LITRES = new Decimal(0.0295735);
+ private static readonly GALLONS_TO_PINTS = new Decimal(8);
+ private static readonly PINTS_TO_GALLONS = new Decimal(0.125);
+ private static readonly GALLONS_TO_MILLILITRES = new Decimal(3785.41);
+ private static readonly MILLILITRES_TO_GALLONS = new Decimal(0.000264172);
+ private static readonly PINTS_TO_FLUID_OUNCES = new Decimal(20);
+ private static readonly FLUID_OUNCES_TO_PINTS = new Decimal(0.05);
+ private static readonly MILLILITRE_TO_FLUID_OUNCE = new Decimal(0.033814);
+ private static readonly LITRE_TO_GALLON = new Decimal(0.264172);
+ private static readonly MILLILITRE_TO_PINT = new Decimal(0.00176);
+
+ static litresToPints(litres: number): number {
+ const result = new Decimal(litres).mul(this.LITRES_TO_PINTS);
+ return Number(result.toFixed(5));
+ }
+
+ static pintsToLitres(pints: number): number {
+ const result = new Decimal(pints).mul(this.PINTS_TO_LITRES);
+ return Number(result.toFixed(5));
+ }
+
+ static litresToFluidOunces(litres: number): number {
+ const result = new Decimal(litres).mul(this.LITRES_TO_FLUID_OUNCES);
+ return Number(result.toFixed(5));
+ }
+
+ static fluidOuncesToLitres(fluidOunces: number): number {
+ const result = new Decimal(fluidOunces).mul(this.FLUID_OUNCES_TO_LITRES);
+ return Number(result.toFixed(5));
+ }
+
+ static gallonsToPints(gallons: number): number {
+ const result = new Decimal(gallons).mul(this.GALLONS_TO_PINTS);
+ return Number(result.toFixed(5));
+ }
+
+ static pintsToGallons(pints: number): number {
+ const result = new Decimal(pints).mul(this.PINTS_TO_GALLONS);
+ return Number(result.toFixed(5));
+ }
+
+ static gallonsToMillilitres(gallons: number): number {
+ const result = new Decimal(gallons).mul(this.GALLONS_TO_MILLILITRES);
+ return Number(result.toFixed(5));
+ }
+
+ static millilitresToGallons(millilitres: number): number {
+ const result = new Decimal(millilitres).mul(this.MILLILITRES_TO_GALLONS);
+ return Number(result.toFixed(5));
+ }
+
+ static pintsToFluidOunces(pints: number): number {
+ const result = new Decimal(pints).mul(this.PINTS_TO_FLUID_OUNCES);
+ return Number(result.toFixed(5));
+ }
+
+ static fluidOuncesToPints(fluidOunces: number): number {
+ const result = new Decimal(fluidOunces).mul(this.FLUID_OUNCES_TO_PINTS);
+ return Number(result.toFixed(5));
+ }
+
+ static millilitresToFluidOunces(millilitres: number): number {
+ const result = new Decimal(millilitres).mul(this.MILLILITRE_TO_FLUID_OUNCE);
+
+ return Number(result.toFixed(5));
+ }
+
+ static fluidOuncesToMillilitres(fluidOunces: number): number {
+ const result = new Decimal(fluidOunces).div(this.MILLILITRE_TO_FLUID_OUNCE);
+
+ return Number(result.toFixed(5));
+ }
+
+ static litresToGallons(litres: number): number {
+ const result = new Decimal(litres).mul(this.LITRE_TO_GALLON);
+
+ return Number(result.toFixed(5));
+ }
+
+ static litresToMillilitres(litres: number): number {
+ const result = new Decimal(litres).mul(1000);
+
+ return Number(result.toFixed(5));
+ }
+
+ static millilitresToLitres(millilitres: number): number {
+ const result = new Decimal(millilitres).div(1000);
+
+ return Number(result.toFixed(5));
+ }
+
+ static gallonsToLitres(gallons: number): number {
+ const result = new Decimal(gallons).div(this.LITRE_TO_GALLON);
+
+ return Number(result.toFixed(5));
+ }
+
+ static millilitresToPints(millilitres: number): number {
+ const result = new Decimal(millilitres).mul(this.MILLILITRE_TO_PINT);
+
+ return Number(result.toFixed(5));
+ }
+
+ static pintsToMillilitres(pints: number): number {
+ const result = new Decimal(pints).div(this.MILLILITRE_TO_PINT);
+
+ return Number(result.toFixed(5));
+ }
+
+ static gallonsToFluidOunces(gallons: number): number {
+ const litres = this.gallonsToLitres(gallons);
+ const millilitres = this.litresToMillilitres(litres);
+
+ return this.millilitresToFluidOunces(millilitres);
+ }
+
+ static fluidOuncesToGallons(fluidOunces: number): number {
+ const millilitres = this.fluidOuncesToMillilitres(fluidOunces);
+ const litres = this.millilitresToLitres(millilitres);
+
+ return this.litresToGallons(litres);
+ }
+}
diff --git a/static/assets/home-icon.png b/static/assets/home-icon.png
deleted file mode 100644
index c4616ca..0000000
Binary files a/static/assets/home-icon.png and /dev/null differ
diff --git a/static/assets/menu-icon.png b/static/assets/menu-icon.png
deleted file mode 100644
index 448a82f..0000000
Binary files a/static/assets/menu-icon.png and /dev/null differ
diff --git a/static/script.js b/static/script.js
deleted file mode 100644
index 3055e8d..0000000
--- a/static/script.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Adds a global event listener to the specified parent element.
- * This is lifted from the Web Dev Simplified YouTube channel.
- *
- * @link https://www.youtube.com/watch?v=cOoP8-NPLSo
- *
- * @param {string} type - The type of event to listen for.
- * @param {string} selector - The CSS selector for the target element(s).
- * @param {function} callback - The callback function to be executed when the event is triggered.
- * @param {HTMLElement} [parent=document] - The parent element to which the event listener is attached.
- */
-function addGlobalEventListener(type, selector, callback, parent = document) {
- parent.addEventListener(type, (event) => {
- if (event.target.matches(selector)) {
- callback(event);
- }
- });
-}
-
-/**
- * Loads the preferred colour scheme from local storage or the user's system settings and sets the theme accordingly.
- * @param themeToggleCheckbox {HTMLInputElement} - An of type checkbox that toggles the theme.
- */
-function loadPreferredColourScheme(themeToggleCheckbox) {
- const currentTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
- document.documentElement.setAttribute('data-theme', currentTheme);
- themeToggleCheckbox.checked = currentTheme === 'dark';
-}
-
-const switchTheme = {
- type: 'change',
- selector: '#theme-switch',
- callback: (event) => {
- const newTheme = event.target.checked ? 'dark' : 'light';
- document.documentElement.setAttribute('data-theme', newTheme);
- localStorage.setItem('theme', newTheme);
- },
- add: function(){
- addGlobalEventListener(this.type, this.selector, this.callback)
- }
-};
-
-const toggleNavPopover = {
- type: 'change',
- selector: 'select',
- callback: (event) => {
- const selectedClass = event.target.className;
- if (selectedClass.match('page-select')) {
- const navMenuPopover = document.getElementById('nav-menu-popover');
- navMenuPopover.togglePopover();
- }
- },
- add: function(){
- addGlobalEventListener(this.type, this.selector, this.callback)
- }
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- const themeSwitch = document.getElementById('theme-switch');
- loadPreferredColourScheme(themeSwitch);
- switchTheme.add();
- toggleNavPopover.add();
-});
\ No newline at end of file
diff --git a/static/style.css b/static/style.css
deleted file mode 100644
index 654224d..0000000
--- a/static/style.css
+++ /dev/null
@@ -1,133 +0,0 @@
-@import url('./styles/variables.css');
-@import url('./styles/theme.css');
-@import url('./styles/utility.css');
-@import url('./styles/button.css');
-@import url('./styles/chip.css');
-@import url('./styles/input.css');
-@import url('./styles/globals.css');
-@import url('./styles/modal.css');
-@import url('./styles/nav.css');
-@import url('./styles/switch.css');
-
-/* Responsive design */
-@media (min-width: 1024px) {
- body {
- font-size: 1.125rem;
- }
-
- main {
- max-width: 1200px;
- padding: 2rem;
- }
-
- header {
- text-align: center;
- }
-
- section {
- padding: 2rem;
- }
-
- h1 {
- font-size: 2.5rem;
- }
-
- h2 {
- font-size: 2rem;
- }
-
- h3 {
- font-size: 1.75rem;
- }
-
- button {
- font-size: 1.25rem;
- padding: 1.5rem;
- }
-
- input[type="text"],
- textarea {
- font-size: 1rem;
- }
-}
-
-@media (max-width: 660px) {
- #nav-menu-popover:popover-open {
- width: 100%;
- height: fit-content;
- top: 0;
- left: 0;
- flex-direction: column;
- justify-content: space-evenly;
- align-items: center;
- }
-
-}
-
-@media (max-width: 600px) {
- body {
- font-size: var(--font-size-small);
- }
-
- h1 {
- font-size: var(--font-size-large);
- }
-
- main {
- padding: 0.5rem;
- }
-
- section {
- padding: 0.5rem;
- }
-
- button {
- padding: 1rem;
- }
-
- input[type="text"],
- textarea {
- font-size: var(--font-size-small);
- }
-}
-
-/* Chrome Mobile Breakpoint */
-@media (max-width: 418px) {
- h1 {
- font-size: 1.25rem;
- }
-
- h2 {
- font-size: 1rem;
- }
-
- h3 {
- font-size: 0.875rem;
- }
-
- h3.nav-title {
- display: none;
- }
-
- button {
- font-size: 0.875rem;
- }
-
- input[type="text"],
- textarea {
- font-size: 0.75rem;
- }
-
- nav {
- flex-direction: column;
- justify-content: center;
- align-items: center;
- }
-}
-
-/* If the user prefers a smooth scroll make it smooth */
-@media(prefers-reduced-motion: no-preference) {
- html {
- scroll-behavior: smooth;
- }
-}
\ No newline at end of file
diff --git a/static/styles/button.css b/static/styles/button.css
deleted file mode 100644
index fee9579..0000000
--- a/static/styles/button.css
+++ /dev/null
@@ -1,14 +0,0 @@
-button {
- background-color: var(--button-background);
- color: var(--button-color);
- border: none;
- border-radius: var(--button-border-radius);
- padding: var(--button-padding);
- font-size: var(--button-font-size);
- cursor: pointer;
- transition: background-color 0.3s ease;
-}
-
-button:hover {
- background-color: var(--button-hover-background);
-}
\ No newline at end of file
diff --git a/static/styles/chip.css b/static/styles/chip.css
deleted file mode 100644
index cdeee43..0000000
--- a/static/styles/chip.css
+++ /dev/null
@@ -1,79 +0,0 @@
-.chip {
- display: inline-flex;
- align-items: center;
- padding: 0 8px;
- height: 24px;
- font-size: var(--font-size-small);
- line-height: 24px;
- border-radius: 12px;
- background-color: var(--secondary-color);
- color: var(--text-color);
- margin: 4px;
- font-family: var(--font-family), serif;
- border: 1px solid var(--input-border);
- transition: background-color 0.3s ease;
-}
-
-.chip:hover {
- background-color: var(--input-background);
-}
-
-.chip-label {
- padding-right: 6px;
- padding-left: 2px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.chip-delete {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 18px;
- height: 18px;
- border: none;
- background-color: var(--button-background);
- color: var(--button-color);
- font-size: 12px;
- cursor: pointer;
- padding: 0;
- border-radius: 50%;
- margin-left: 4px;
- transition: background-color 0.3s ease;
-}
-
-.chip-delete:hover {
- cursor: pointer;
- background-color: var(--button-hover-background);
-}
-
-.chip-delete:focus {
- outline: none;
- box-shadow: 0 0 0 2px var(--card-shadow);
-}
-
-button.chip-delete {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 22px;
- height: 22px;
- border: none;
- background-color: #9e9e9e;
- color: #e0e0e0;
- font-size: 14px;
- cursor: pointer;
- padding: 0;
- border-radius: 50%;
- margin-left: 4px;
-}
-
-button.chip-delete:hover {
- background-color: #757575;
-}
-
-button.chip-delete:focus {
- outline: none;
- box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
-}
diff --git a/static/styles/globals.css b/static/styles/globals.css
deleted file mode 100644
index 2eec7e0..0000000
--- a/static/styles/globals.css
+++ /dev/null
@@ -1,67 +0,0 @@
-body {
- font-family: var(--font-family), serif;
- font-size: var(--font-size-base);
- line-height: 1.6;
- color: var(--text-color);
- background-color: var(--secondary-color);
- margin: 0;
- padding: 0;
-}
-
-main {
- max-width: 800px;
- margin: 0 auto;
- padding: 1rem;
-}
-
-header {
- text-align: center;
- margin-bottom: 2rem;
- position: sticky;
- top: 0;
- background-color: var(--secondary-color);
- z-index: 1000;
-}
-
-footer {
- background-color: var(--secondary-color);
- color: var(--text-color);
- text-align: center;
- padding: 1rem;
- position: sticky;
- bottom: 0;
- z-index: 0;
-}
-
-h1,
-h2,
-h3 {
- color: var(--primary-color);
- text-align: center;
-}
-
-p {
- margin-bottom: 1rem;
-}
-
-label {
- display: block;
- margin-bottom: 0.5rem;
-}
-
-a {
- color: var(--link-color);
- text-decoration: none;
- transition: color 0.3s ease;
-}
-
-a:hover {
- color: var(--link-hover-color);
-}
-
-code {
- background-color: var(--secondary-color);
- color: var(--text-color);
- padding: 5px;
- border-radius: 4px;
-}
\ No newline at end of file
diff --git a/static/styles/input.css b/static/styles/input.css
deleted file mode 100644
index b58b916..0000000
--- a/static/styles/input.css
+++ /dev/null
@@ -1,24 +0,0 @@
-input[type="text"],
-input[type="colour"],
-select,
-textarea {
- min-width: 8rem;
- width: fit-content;
- padding: 10px;
- margin: 8px 0;
- box-sizing: border-box;
- border: 1px solid var(--input-border);
- border-radius: 4px;
- font-size: 1rem;
- background: var(--input-background);
- color: var(--input-text-color);
- transition: border-color 0.3s ease;
-}
-
-input[type="text"]:focus,
-select:focus,
-input[type="colour"]:focus,
-textarea:focus {
- border-color: var(--primary-color);
- outline: transparent;
-}
\ No newline at end of file
diff --git a/static/styles/modal.css b/static/styles/modal.css
deleted file mode 100644
index 8dc8323..0000000
--- a/static/styles/modal.css
+++ /dev/null
@@ -1,22 +0,0 @@
-#modal-dialog {
- background: var(--secondary-color);
- color: var(--text-color);
-}
-
-/* Distinguish between the top and bottom layers when opening a modal dialog */
-#modal-dialog::backdrop {
- background: #fff5;
- backdrop-filter: blur(4px);
-}
-
-/* Transition the dialog (currently only in firefox & chromium) */
-#modal-dialog {
- opacity: 1;
- transform: scale(1);
- transition: all 0.5s ease-in-out;
-
- @starting-style {
- opacity: 0;
- transform: scale(0.8);
- }
-}
\ No newline at end of file
diff --git a/static/styles/nav.css b/static/styles/nav.css
deleted file mode 100644
index f96e8d1..0000000
--- a/static/styles/nav.css
+++ /dev/null
@@ -1,78 +0,0 @@
-#nav-menu-popover:popover-open {
- width: 50%;
- position: absolute;
- inset: unset;
- top: 0;
- left: 2.65rem;
- margin: 0;
- display: flex;
- border: none;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- background-color: var(--card-background);
-}
-
-nav {
- background-color: var(--card-background);
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- height: 6rem;
-
- img.icon {
- height: 1.65rem;
- display: block;
- border-radius: 50%;
- background-color: var(--button-background);
- }
-
- button.round-icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- margin: 1rem;
- border-radius: 50%;
- background-color: var(--button-background);
- color: var(--button-color);
- border: none;
- height: 1.65rem; /* Adjust size as needed */
- }
-
- button.round-icon img {
- /*width: 1.65rem; !* Adjust size as needed *!*/
- /*border-radius: 50%;*/
- object-fit: cover;
- }
-
- .active {
- float: left;
- display: block;
- border-radius: 50%;
- text-align: center;
- padding: 0 10px;
- height: 1.65rem;
- text-decoration: none;
- background-color: var(--button-background);
- color: var(--button-color);
- }
-
- .active:hover {
- background-color: var(--button-hover-background);
- color: var(--button-color)
- }
-
- label {
- display: flex;
- flex-direction: column-reverse;
- }
-
- label>span {
- text-align: center;
- font-size: small;
- transform-origin: top center;
- color: var(--button-color)
- }
-}
\ No newline at end of file
diff --git a/static/styles/switch.css b/static/styles/switch.css
deleted file mode 100644
index 71afce3..0000000
--- a/static/styles/switch.css
+++ /dev/null
@@ -1,46 +0,0 @@
-.switch {
- margin: 1rem;
- appearance: none;
- position: relative;
- display: inline-block;
- background: lightgrey;
- height: 1.65rem;
- width: 2.75rem;
- vertical-align: middle;
- border-radius: 2rem;
- box-shadow: 0 1px 3px var(--card-shadow) inset;
- transition: 0.25s linear background;
-}
-
-.switch::before {
- content: '';
- display: block;
- width: 1.25rem;
- height: 1.25rem;
- background: #fff;
- border-radius: 1.2rem;
- position: absolute;
- top: 0.2rem;
- left: 0.2rem;
- box-shadow: 0 1px 3px #0003;
- transition: 0.25s linear transform;
- transform: translateX(0rem);
-}
-
-.switch:checked {
- background: var(--primary-color);
-}
-
-.switch:not(:checked),
-.switch:not(:checked):focus-visible {
- background-color: var(--primary-color);
-}
-
-.switch:checked::before {
- transform: translateX(1rem);
-}
-
-.switch:focus-visible {
- outline: 2px solid var(--primary-color);
- outline-offset: 2px;
-}
\ No newline at end of file
diff --git a/static/styles/theme.css b/static/styles/theme.css
deleted file mode 100644
index 9b6f653..0000000
--- a/static/styles/theme.css
+++ /dev/null
@@ -1,33 +0,0 @@
-[data-theme="light"] {
- --text-color: #1a1a1a;
- --primary-color: #6200ea;
- --secondary-color: #e0e0e0;
- --card-background: #ffffff;
- --button-background: #6200ea;
- --card-border: #b0b0b0;
- --card-shadow: rgba(0, 0, 0, 0.1);
- --button-color: #ffffff;
- --button-hover-background: #3700b3;
- --link-color: #6200ea;
- --link-hover-color: #3700b3;
- --input-background: #ffffff;
- --input-border: #8a8a8a;
- --input-text-color: #1a1a1a;
-}
-
-[data-theme="dark"] {
- --text-color: #e0e0e0;
- --primary-color: #1e88e5;
- --secondary-color: #444;
- --card-background: #333;
- --card-border: #222;
- --card-shadow: rgba(0, 0, 0, 0.6);
- --button-background: #1e88e5;
- --button-hover-background: #1565c0;
- --button-color: #ffffff;
- --link-color: #1e88e5;
- --link-hover-color: #1565c0;
- --input-background: #444;
- --input-border: #333;
- --input-text-color: #e0e0e0;
-}
\ No newline at end of file
diff --git a/static/styles/utility.css b/static/styles/utility.css
deleted file mode 100644
index 1c8237a..0000000
--- a/static/styles/utility.css
+++ /dev/null
@@ -1,87 +0,0 @@
-.card {
- background-color: var(--card-background);
- border: 1px solid var(--card-border);
- border-radius: var(--card-radius);
- box-shadow: 0 4px 8px var(--card-shadow);
- padding: 1rem;
- margin-bottom: 2rem;
-}
-
-.centred-row {
- display: flex;
- flex-direction: row;
- gap: 1rem;
- justify-content: center;
-}
-
-.centred-column {
- display: flex;
- flex-direction: column;
- gap: 1rem;
- align-items: center;
-}
-
-.box {
- width: 6rem;
- height: 6rem;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background: var(--button-background);
- overflow: auto;
-}
-
-summary:hover {
- cursor: pointer;
-}
-
-details > summary {
- list-style: none;
-}
-
-.footnote:hover {
- cursor: help;
-}
-
-fieldset {
- display: flex;
- flex-direction: column;
-}
-
-form #other-text {
- display: none;
-}
-
-form:has(#other:checked) #other-text {
- display: block;
-}
-
-.resize-vertical {
- resize: vertical;
-}
-
-.resize-horizontal {
- resize: horizontal;
-}
-
-.resize-both {
- resize: both;
-}
-
-.morph {
- background: var(--secondary-color);
- box-shadow: 0 0 0 transparent, 0 0 0 transparent;
- transition: box-shadow 0.2s ease-in-out;
-}
-
-.morph:hover {
- box-shadow: 12px 12px 12px var(--text-color), -10px -10px 10px var(--secondary-color);
- transition: .2s ease-in-out box-shadow;
-}
-
-.gradient {
- background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet);
- background-clip: text;
- color: transparent;
-}
\ No newline at end of file
diff --git a/static/styles/variables.css b/static/styles/variables.css
deleted file mode 100644
index 31e061c..0000000
--- a/static/styles/variables.css
+++ /dev/null
@@ -1,10 +0,0 @@
-:root {
- --font-family: 'Roboto', sans-serif;
- --font-size-base: 16px;
- --font-size-large: 1.5rem;
- --font-size-small: 0.875rem;
- --card-radius: 8px;
- --button-border-radius: 12px;
- --button-padding: 10px 20px;
- --button-font-size: 1rem;
-}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..238655f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ // Enable latest features
+ "lib": ["ESNext", "DOM"],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ }
+}