Skip to content

Instantly share code, notes, and snippets.

@strix
Last active December 19, 2024 20:23
Show Gist options
  • Save strix/319b17fb835425ed66da063c3e2f252e to your computer and use it in GitHub Desktop.
Save strix/319b17fb835425ed66da063c3e2f252e to your computer and use it in GitHub Desktop.
Currency Calculations
<!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