From 078d74d6c60aabd5ca2b6be3633d4416ea43b6c9 Mon Sep 17 00:00:00 2001 From: Stephan Troyer Date: Mon, 6 Nov 2023 12:24:26 +0100 Subject: [PATCH] add currency translation support --- lib/README.md | 28 +++--- lib/calc.cc | 81 +++++++++++------- website/src/lib/calculator.ts | 39 +++++++-- website/src/lib/calculatorModule.ts | 53 ++++++++++-- website/src/lib/tools.ts | 2 +- website/src/routes/+layout.svelte | 17 ++-- .../src/routes/api/getCurrencyData/+server.ts | 34 ++++++++ website/src/routes/news/+page.svelte | 6 ++ website/src/routes/news/version.ts | 2 +- website/src/service-worker.ts | 37 ++++++-- website/static/calc.js | 2 +- website/static/calc.wasm | Bin 4993889 -> 5000531 bytes website/static/manifest.webmanifest | 4 +- 13 files changed, 232 insertions(+), 73 deletions(-) create mode 100644 website/src/routes/api/getCurrencyData/+server.ts diff --git a/lib/README.md b/lib/README.md index 8d9a78a..756e733 100644 --- a/lib/README.md +++ b/lib/README.md @@ -13,19 +13,19 @@ source ./emsdk_env.sh mkdir -p ~/opt/src cd ~/opt/src -wget https://gmplib.org/download/gmp/gmp-6.1.2.tar.lz -tar xf gmp-6.1.2.tar.lz -cd gmp-6.1.2 -emconfigure ./configure --disable-assembly --host none --enable-cxx --prefix=${HOME}/opt +wget https://gmplib.org/download/gmp/gmp-6.3.0.tar.lz +tar xf gmp-6.3.0.tar.lz +cd gmp-6.3.0 +emconfigure ./configure --disable-assembly --host none --enable-cxx --prefix=${HOME}/opt # no, the "none" host is not obsolete. make make install cd .. -wget https://www.mpfr.org/mpfr-current/mpfr-4.1.0.tar.xz -wget https://www.mpfr.org/mpfr-current/allpatches -tar xf mpfr-4.1.0.tar.xz -cd mpfr-4.1.0 -patch -N -Z -p1 < ../allpatches +wget https://www.mpfr.org/mpfr-current/mpfr-4.2.1.tar.xz # if not found, use a newer version +# wget https://www.mpfr.org/mpfr-current/allpatches # if available +tar xf mpfr-4.2.1.tar.xz +cd mpfr-4.2.1 +# patch -N -Z -p1 < ../allpatches emconfigure ./configure --prefix=${HOME}/opt --with-gmp=${HOME}/opt make make install @@ -33,8 +33,8 @@ cd .. wget ftp://xmlsoft.org/libxml2/libxml2-git-snapshot.tar.gz tar xf libxml2-git-snapshot.tar.gz -cd libxml2-2.9.12/ -emconfigure ./configure --prefix=${HOME}/opt +cd libxml2-2.9.13/ # or whichever version is up to date +emconfigure ./configure --prefix=${HOME}/opt --disable-shared make make install ln -s ${HOME}/opt/include/libxml2/libxml ${HOME}/opt/include/libxml @@ -48,10 +48,10 @@ cd libqalculate sed -i 's/PKG_CHECK_MODULES(LIBCURL, libcurl)/#PKG_CHECK_MODULES(LIBCURL, libcurl)/' configure sed -i 's/PKG_CHECK_MODULES(ICU, icu-uc)/#PKG_CHECK_MODULES(ICU, icu-uc)/' configure sed -i 's/PKG_CHECK_MODULES(LIBXML, libxml-2.0/#PKG_CHECK_MODULES(LIBXML, libxml-2.0/' configure -sed -i 's/$as_echo "#define HAVE_LIBCURL 1" >>confdefs.h/#$as_echo "#define HAVE_LIBCURL 1" >>confdefs.h/' configure -sed -i 's/$as_echo "#define HAVE_ICU 1" >>confdefs.h/#$as_echo "#define HAVE_ICU 1" >>confdefs.h/' configure +sed -i 's/#define HAVE_LIBCURL 1//' configure +sed -i 's/#define HAVE_ICU 1//' configure sed -i 's/#define HAVE_PIPE2 1/#define HAVE_PIPE2 0/' configure -emconfigure ./configure --prefix=${HOME}/opt CPPFLAGS=-I${HOME}/opt/include LDFLAGS="-L${HOME}/opt/lib -lxml2" --without-libcurl --enable-compiled-definitions --disable-nls +emconfigure ./configure --prefix=${HOME}/opt CPPFLAGS=-I${HOME}/opt/include LDFLAGS="-L${HOME}/opt/lib -lxml2" --without-libcurl --enable-compiled-definitions --disable-nls --disable-shared make make install cd .. diff --git a/lib/calc.cc b/lib/calc.cc index d4aa3bf..03d6ca7 100644 --- a/lib/calc.cc +++ b/lib/calc.cc @@ -41,43 +41,75 @@ Calculation calculate(std::string calculation, int timeout = 500, int optionFlag return ret; } -struct VariableInfo +val getVariables() { - std::string name; - std::string description; - std::string aliases; -}; - -std::vector getVariables() -{ - std::vector variables; + auto variables = val::array(); for (auto &variable : calc.variables) { if (!variable->isKnown() || variable->isHidden()) continue; - VariableInfo info; - info.name = variable->preferredDisplayName(true, true).name; - info.description = variable->title(false, true); + auto info = val::object(); + info.set("name", variable->preferredDisplayName(true, true).name); + info.set("description", variable->title(false, true)); auto nameCount = variable->countNames(); + auto aliases = val::array(); if (nameCount < 1) { - info.aliases = variable->preferredDisplayName(true, true).name; + aliases.call("push", variable->preferredDisplayName(true, true).name); } else { for (size_t i = 1; i <= nameCount; i++) { - info.aliases += variable->getName(i).name; - if (i < nameCount) - info.aliases += "\t"; + aliases.call("push", variable->getName(i).name); } } - variables.push_back(info); + info.set("aliases", aliases); + variables.call("push", info); } return variables; } +int updateCurrencyValues(const val ¤cyData, std::string baseCurrency, bool showWarning) +{ + int errorCode = 0; + + auto u1 = CALCULATOR->getActiveUnit(baseCurrency); + if (u1 != calc.u_euro) + { + return 1; + } + + for (int i = 0; i < currencyData["length"].as(); i++) + { + emscripten::val data = currencyData[i]; + auto name = data["name"].as(); + auto value = data["value"].as(); + auto u2 = calculator->getActiveUnit(name); + if (!u2) + { + u2 = calc.addUnit(new AliasUnit(_("Currency"), name, "", "", "", calc.u_euro, "1", 1, "", false, true)); + } + else if (!u2->isCurrency()) + { + errorCode = 2; + continue; + } + + ((AliasUnit *)u2)->setBaseUnit(u1); + ((AliasUnit *)u2)->setExpression(value); + u2->setApproximate(); + u2->setPrecision(-2); + u2->setChanged(false); + } + + calc.setExchangeRatesWarningEnabled(showWarning); + calc.loadGlobalCurrencies(); + + return errorCode; +} + int main() { calc.loadGlobalDefinitions(); @@ -94,12 +126,12 @@ int main() std::string info() { - return "libqalculate by Hanna Knutsson, compiled by Stephan Troyer"; + return "libqalculate by Hanna Knutsson, wrapped & compiled by Stephan Troyer"; } int version() { - return 3; + return 4; } EMSCRIPTEN_BINDINGS(Calculator) @@ -109,6 +141,7 @@ EMSCRIPTEN_BINDINGS(Calculator) function("version", &version); function("getVariables", &getVariables); function("set_option", &set_option); + function("updateCurrencyValues", &updateCurrencyValues); } EMSCRIPTEN_BINDINGS(calculation) @@ -119,13 +152,3 @@ EMSCRIPTEN_BINDINGS(calculation) .property("output", &Calculation::output) .property("messages", &Calculation::messages); } - -EMSCRIPTEN_BINDINGS(variableInfo) -{ - class_("VariableInfo") - .constructor<>() - .property("name", &VariableInfo::name) - .property("description", &VariableInfo::description) - .property("aliases", &VariableInfo::aliases); - register_vector("vector"); -} diff --git a/website/src/lib/calculator.ts b/website/src/lib/calculator.ts index 43f7380..ea69e4c 100644 --- a/website/src/lib/calculator.ts +++ b/website/src/lib/calculator.ts @@ -2,6 +2,7 @@ import { calculate, initializeCalculationModule, setOption, + updateCurrencyValues, } from './calculatorModule'; import { History } from './history'; import { Settings } from './settings'; @@ -19,6 +20,12 @@ export interface Calculation { bookmarkName?: string; } +export interface CurrencyData { + base: string; + date: string; + rates: Record; +} + /** Parses the status message coming from Web Assembly */ function parseCalculationMessages(messagesString: string): { messages: string[]; @@ -102,7 +109,7 @@ export class Calculator { }; } - private pendingCalculationOnceLoaded: string | null = null; + #pendingCalculationOnceLoaded: string | null = null; submitCalculation(input: string) { const isSetCommand = input.startsWith('set '); @@ -110,7 +117,7 @@ export class Calculator { this.submittedListeners.forEach((l) => l(input)); } if (!this.isLoaded) { - this.pendingCalculationOnceLoaded = input; + this.#pendingCalculationOnceLoaded = input; return; } @@ -121,6 +128,23 @@ export class Calculator { } } + #pendingCurrencyData: CurrencyData | null = null; + + updateCurrencyData(data: CurrencyData) { + if (!this.isLoaded) { + this.#pendingCurrencyData = data; + return; + } + updateCurrencyValues( + Object.entries(data.rates).map(([name, value]) => ({ + name, + value, + })), + data.base, + new Date(data.date), + ); + } + constructor() { if (typeof window !== 'undefined') { // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -129,9 +153,14 @@ export class Calculator { this.settings.apply(); - if (this.pendingCalculationOnceLoaded) { - this.submitCalculation(this.pendingCalculationOnceLoaded); - this.pendingCalculationOnceLoaded = null; + if (this.#pendingCurrencyData) { + this.updateCurrencyData(this.#pendingCurrencyData); + this.#pendingCurrencyData = null; + } + + if (this.#pendingCalculationOnceLoaded) { + this.submitCalculation(this.#pendingCalculationOnceLoaded); + this.#pendingCalculationOnceLoaded = null; } this.loadedListeners.forEach((l) => l()); }); diff --git a/website/src/lib/calculatorModule.ts b/website/src/lib/calculatorModule.ts index 42a0a5e..dfd7dac 100644 --- a/website/src/lib/calculatorModule.ts +++ b/website/src/lib/calculatorModule.ts @@ -16,6 +16,11 @@ export interface VariableDefinition { aliases: string[]; } +export interface CurrencyDataset { + name: string; + value: number; +} + let Version: number | undefined; export function version(): number { if (!Version) Version = Module.version?.() ?? 1; @@ -47,6 +52,21 @@ export function setOption(option: string): boolean { return Module.set_option(option); } +export function updateCurrencyValues( + data: CurrencyDataset[], + baseCurrency: string, + updateDate: Date, +): boolean { + if (version() < 4) return false; + return ( + Module.updateCurrencyValues( + data, + baseCurrency, + +new Date() - +new Date(updateDate) > 7 * 24 * 3600 * 1000, // 7 days + ) === 0 + ); +} + export function info(): string { return Module.info(); } @@ -86,14 +106,29 @@ export async function initializeCalculationModule() { if (Module.getVariables) { // parse the variables supported by libqalculate - const vars = Module.getVariables(); - Variables = wasmVectorToArray(vars) - .map((v: any) => ({ - name: v.name as string, - description: v.description as string, - aliases: (v.aliases as string).split('\t'), - })) - .filter((v) => !['true', 'false', 'undefined'].includes(v.name)); - vars.delete(); + const vars = Module.getVariables() as Array<{ + name: string; + description: string; + aliases: string[]; + }>; + if (version() < 4) { + Variables = wasmVectorToArray(vars) + .map((v: any) => ({ + name: v.name, + description: v.description, + aliases: v.aliases.split('\t'), + })) + .filter( + (v) => + !['true', 'false', 'undefined'].includes( + v.name as string, + ), + ); + (vars as any).delete(); + } else { + Variables = vars.filter( + (v) => !['true', 'false', 'undefined'].includes(v.name), + ); + } } } diff --git a/website/src/lib/tools.ts b/website/src/lib/tools.ts index c416543..c86d3c8 100644 --- a/website/src/lib/tools.ts +++ b/website/src/lib/tools.ts @@ -17,7 +17,7 @@ export function getOS(): if (navigator.userAgent.includes('Linux')) return 'linux'; } -export function wasmVectorToArray(vector: any) { +export function wasmVectorToArray(vector: any): T[] { const array = []; for (let i = 0; i < vector.size(); i++) { array.push(vector.get(i)); diff --git a/website/src/routes/+layout.svelte b/website/src/routes/+layout.svelte index 6b1ba02..3f81418 100644 --- a/website/src/routes/+layout.svelte +++ b/website/src/routes/+layout.svelte @@ -60,14 +60,21 @@ }); }); }); + navigator.serviceWorker?.addEventListener( 'message', - (event: MessageEvent<{ type: string }>) => { - if (event.data?.type === 'reload') { - window.location.reload(); + (event: MessageEvent<{ type: string, data?: any }>) => { + switch (event.data?.type) { + case 'reload': window.location.reload(); break; + case 'updateCurrencyData': calculator.updateCurrencyData(event.data?.data); break; + default: throw new Error(`Unknown message ${event.data?.type}`); } }, ); + + fetch('/api/getCurrencyData').then(async (response) => + calculator.updateCurrencyData(await response.json()) + ); } let isTouchScreen = false; function touchstart() { @@ -85,7 +92,7 @@
-

Qalculator

+

Qalculator.xyz

- An update is available, click to restart Qalculator. + An update is available, click to restart Qalculator.xyz.

{/if} diff --git a/website/src/routes/api/getCurrencyData/+server.ts b/website/src/routes/api/getCurrencyData/+server.ts new file mode 100644 index 0000000..3c6d17c --- /dev/null +++ b/website/src/routes/api/getCurrencyData/+server.ts @@ -0,0 +1,34 @@ +import type { CurrencyData } from '$lib/calculator'; +import { EXCHANGERATE_KEY } from '$env/static/private'; + +export async function GET(): Promise { + if (!EXCHANGERATE_KEY) { + throw new Error('EXCHANGERATE_KEY not set'); + } + + const resp = await fetch( + `http://api.exchangerate.host/live?source=EUR&access_key=${EXCHANGERATE_KEY}`, + ); + if (!resp.ok) + throw new Error(`error fetching currency data: ${resp.statusText}`); + + const data = await resp.json(); + + const body = { + date: new Date(data.timestamp * 1000 - 10*24*3600*1000).toISOString().split('T')[0], + base: data.source, + rates: Object.fromEntries( + Object.entries(data.quotes as Record).map( + ([name, value]) => [name.substring(3), (1 / +value).toString()], + ), + ), + } as CurrencyData; + + return new Response(JSON.stringify(body), { + headers: { + 'Content-Type': 'application/json', // check casing + 'Access-Control-Allow-Origin': '*', + 'cache-control': `public, max-age=${6 * 3600}`, + }, + }); +} diff --git a/website/src/routes/news/+page.svelte b/website/src/routes/news/+page.svelte index 862a839..95fefb9 100644 --- a/website/src/routes/news/+page.svelte +++ b/website/src/routes/news/+page.svelte @@ -8,6 +8,12 @@

News

+

2023-11-05 Currency translation

+
    +
  • Currency translations are now supported.
  • +
  • libqalculate (the library behind this app) got updated
  • +
+

2023-04-02 Additional settings