Last active
December 19, 2024 20:23
-
-
Save strix/319b17fb835425ed66da063c3e2f252e to your computer and use it in GitHub Desktop.
Currency Calculations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Currency Calculations</title> | |
| <script src="https://unpkg.com/[email protected]/dist/web/js-big-decimal.min.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="preconnect" href="https://fonts.bunny.net"> | |
| <link href="https://fonts.bunny.net/css?family=inter:400,500,600&display=swap" rel="stylesheet" /> | |
| <style type="text/tailwindcss"> | |
| @layer base { | |
| @font-face { | |
| font-famly: 'Inter'; | |
| src: url('https://fonts.bunny.net/css?family=inter:400,500,600&display=swap'); | |
| } | |
| } | |
| </style> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| fontFamily: { | |
| sans: [ | |
| 'Inter', | |
| 'sans-serif', | |
| ], | |
| }, | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <div id="app" class="dark:bg-gray-900"> | |
| <div class="min-h-screen flex flex-col"> | |
| <header class="bg-blue-600 text-white py-4"> | |
| <div class="container mx-auto px-4"> | |
| <h1 class="text-lg font-bold">Currency Calculations</h1> | |
| </div> | |
| </header> | |
| <div class="flex-grow container mx-auto px-4 py-6"> | |
| <div class="max-w-full mx-auto"> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <div class="max-w-sm"> | |
| <h2 class="text-2xl mb-4 text-gray-900 dark:text-white">Exchange Rates</h2> | |
| <button @click="randomizeExchangeRates" type="button" | |
| class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Randomize | |
| exchange rates</button> | |
| <div v-for="(currencyExchange, i) in currencyExchanges" class="mb-5"> | |
| <label for="currencyExchange{{ i + 1 }}" | |
| class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Currency exchange rate {{ i + 1 | |
| }}</label> | |
| <input v-model="currencyExchanges[i]" type="text" id="currencyExchange{{ i + 1 }}" | |
| class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" /> | |
| </div> | |
| <div class="flex justify-between"> | |
| <button @click="runCalculations" type="button" | |
| class="px-6 py-3.5 text-base font-medium text-white inline-flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Calculate</button> | |
| <button @click="copyMe" type="button" | |
| class="py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">{{ | |
| shareText }}</button> | |
| </div> | |
| </div> | |
| <div class="max-w-sm"> | |
| <h2 class="text-2xl mb-4 text-gray-900 dark:text-white">Items</h2> | |
| <div class="flex justify-between"> | |
| <button @click="randomizeAmounts" type="button" | |
| class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Randomize | |
| amounts</button> | |
| <button @click="addFormField" type="button" | |
| class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Add | |
| amount field</button> | |
| </div> | |
| <div v-for="(formField, i) in formFields" class="mb-5"> | |
| <label for="item{{ i + 1 }}" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Item | |
| {{ i + 1 }}</label> | |
| <input v-model="formFields[i]" type="text" id="item{{ i + 1 }}" | |
| class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div v-for="(result, i) in results" | |
| class="w-full mb-4 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"> | |
| <div class="p-4 bg-white rounded-lg md:p-8 dark:bg-gray-800"> | |
| <div class="flex justify-between items-center"> | |
| <h2 class="mb-3 text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white">{{ result.sequence | |
| }}</h2> | |
| <button v-if="!amountMatchesGroundTruth(result)" @click="updateGroundTruth(result.result)" type="button" | |
| class="px-3 py-2 text-xs font-medium text-center inline-flex items-center text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"> | |
| Set as ground truth | |
| </button> | |
| </div> | |
| <div class="mb-3 text-2xl font-medium text-gray-900 dark:text-white"> | |
| <div v-if="amountMatchesGroundTruth(result)" | |
| class="flex items-center p-4 mb-4 text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400" | |
| role="alert"> | |
| <svg class="flex-shrink-0 inline w-6 h-6 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" | |
| fill="currentColor" viewBox="0 0 24 24"> | |
| <path fill-rule="evenodd" | |
| d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm13.707-1.293a1 1 0 0 0-1.414-1.414L11 12.586l-1.793-1.793a1 1 0 0 0-1.414 1.414l2.5 2.5a1 1 0 0 0 1.414 0l4-4Z" | |
| clip-rule="evenodd" /> | |
| </svg> | |
| <div> | |
| <span class="font-medium">{{ result.result.getValue() }}</span> | |
| </div> | |
| </div> | |
| <div v-else | |
| class="flex items-center p-4 mb-4 text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" | |
| role="alert"> | |
| <svg class="flex-shrink-0 inline w-6 h-6 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" | |
| fill="currentColor" viewBox="0 0 24 24"> | |
| <path fill-rule="evenodd" | |
| d="M8.97 14.316H5.004c-.322 0-.64-.08-.925-.232a2.022 2.022 0 0 1-.717-.645 2.108 2.108 0 0 1-.242-1.883l2.36-7.201C5.769 3.54 5.96 3 7.365 3c2.072 0 4.276.678 6.156 1.256.473.145.925.284 1.35.404h.114v9.862a25.485 25.485 0 0 0-4.238 5.514c-.197.376-.516.67-.901.83a1.74 1.74 0 0 1-1.21.048 1.79 1.79 0 0 1-.96-.757 1.867 1.867 0 0 1-.269-1.211l1.562-4.63ZM19.822 14H17V6a2 2 0 1 1 4 0v6.823c0 .65-.527 1.177-1.177 1.177Z" | |
| clip-rule="evenodd" /> | |
| </svg> | |
| <div> | |
| <span class="font-medium">{{ result.result.getValue() }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <details> | |
| <summary class="mb-4 text-gray-900 dark:text-white cursor-pointer">Show calculation details</summary> | |
| <div v-for="(log, i) in result.logs" | |
| class="mb-2 p-4 border border-gray-200 rounded dark:border-gray-700 text-gray-900 dark:text-white"> | |
| <p class="mb-2 text-gray-900 font-semibold dark:text-white"> | |
| {{ result.sequence.split(', ')[i] }} | |
| </p> | |
| <p class="text-sm text-gray-900 dark:text-white"> | |
| {{ log }} | |
| </p> | |
| </div> | |
| </details> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import {createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js' | |
| const getRandomNumber = () => (Math.random() * (100.120 - 1.02000) + 1.02000).toFixed(5) | |
| const makeConvert = (exchangeRate, first) => { | |
| if (first) { | |
| const conversion1 = (values) => values.map(value => value.multiply(exchangeRate)) | |
| return conversion1 | |
| } | |
| const conversion2 = (values) => values.map(value => value.multiply(exchangeRate)) | |
| return conversion2 | |
| } | |
| // const convert = (values) => values.map(value => value.multiply(currency1)) | |
| const sum = (values) => ([values.reduce((accum, value) => accum.add(value), new bigDecimal(0))]) | |
| const round = (values) => values.map(value => value.round(2)) | |
| const permute = (permutation) => { | |
| const length = permutation.length | |
| const result = [permutation.slice()] | |
| const c = new Array(length).fill(0) | |
| let i = 1, k, p | |
| while (i < length) { | |
| if (c[i] < i) { | |
| k = i % 2 && c[i] | |
| p = permutation[i] | |
| permutation[i] = permutation[k] | |
| permutation[k] = p | |
| ++c[i] | |
| i = 1 | |
| result.push(permutation.slice()) | |
| } else { | |
| c[i] = 0 | |
| ++i | |
| } | |
| } | |
| return result | |
| } | |
| const exec = (result, funcs, logs) => { | |
| if (funcs.length === 0) { | |
| return [result, logs] | |
| } | |
| const [first, ...rest] = funcs | |
| const newResult = first(result) | |
| logs.push(`${result.map(num => num.getValue()).join(', ')} -> ${newResult.map(num => num.getValue()).join(', ')}`) | |
| return exec(newResult, rest, logs) | |
| } | |
| createApp({ | |
| mounted() { | |
| const shareUrl = new URL(window.location) | |
| const hash = shareUrl.searchParams.get('state') | |
| if (!hash) { | |
| return | |
| } | |
| const state = JSON.parse(atob(hash)) | |
| this.groundTruthValue = new bigDecimal(state.groundTruthValue) | |
| this.currencyExchanges = state.currencyExchanges | |
| this.formFields = state.formFields | |
| this.runCalculations() | |
| }, | |
| data() { | |
| let currency1 = 0.0965 | |
| let currency2 = 1.33 | |
| return { | |
| groundTruthValue: new bigDecimal(0), | |
| randomNumbers: false, | |
| currencyExchanges: [currency1, currency2], | |
| formFields: new Array(7).fill(0), | |
| results: [], | |
| shareText: 'Share results', | |
| } | |
| }, | |
| computed: { | |
| populatedFormFields() { | |
| return this.formFields.map(n => getRandomNumber()) | |
| }, | |
| shareUrl() { | |
| const hash = JSON.stringify({ | |
| groundTruthValue: this.groundTruthValue.getValue(), | |
| currencyExchanges: this.currencyExchanges, | |
| formFields: this.formFields, | |
| }) | |
| const shareUrl = new URL(window.location) | |
| shareUrl.searchParams.set('state', btoa(hash)) | |
| return shareUrl.toString() | |
| }, | |
| }, | |
| methods: { | |
| addFormField() { | |
| return this.formFields.push(0) | |
| }, | |
| randomizeAmounts() { | |
| for (let i = 0; i < this.formFields.length; i++) { | |
| this.formFields[i] = getRandomNumber() | |
| } | |
| }, | |
| randomizeExchangeRates() { | |
| for (let i = 0; i < this.currencyExchanges.length; i++) { | |
| this.currencyExchanges[i] = getRandomNumber() | |
| } | |
| }, | |
| runCalculations() { | |
| this.results = [] | |
| const currency1 = new bigDecimal(this.currencyExchanges[0]) | |
| const currency2 = new bigDecimal(this.currencyExchanges[1]) | |
| const subtotals = this.formFields.map(v => new bigDecimal(v)) | |
| const permutations = permute([makeConvert(currency1, true), makeConvert(currency2, false), sum, round]) | |
| for (const funcs of permutations) { | |
| const sequence = funcs.map(f => f.name).join(', ') | |
| const [[result], logs] = exec(Object.values(subtotals), funcs, []) | |
| this.results.push({sequence, result, logs}) | |
| } | |
| this.groundTruthValue = this.results[0].result | |
| }, | |
| updateGroundTruth(result) { | |
| this.groundTruthValue = result.round(2) | |
| }, | |
| amountMatchesGroundTruth(result) { | |
| return result.result.round(2).compareTo(this.groundTruthValue) === 0 | |
| }, | |
| copyMe() { | |
| navigator.clipboard.writeText(this.shareUrl) | |
| const beforeText = this.shareText | |
| this.shareText = 'Copied!' | |
| setTimeout(() => this.shareText = beforeText, 1000) | |
| }, | |
| } | |
| }).mount('#app') | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment