From d815a7973a87c173439749225ddb10470d9f55a8 Mon Sep 17 00:00:00 2001 From: Tomas Savigliano Date: Sun, 20 Oct 2024 16:27:07 -0700 Subject: [PATCH] Started google sheets implementation. --- .../extensions/googleSheets/package-lock.json | 8 +- .../extensions/googleSheets/package.json | 2 +- .../extensions/googleSheets/usable/index.js | 158 ++++++++++++++++++ repositories/query/typescript/README.md | 53 +++++- repositories/query/typescript/package.json | 2 +- 5 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 repositories/extensions/googleSheets/usable/index.js diff --git a/repositories/extensions/googleSheets/package-lock.json b/repositories/extensions/googleSheets/package-lock.json index 960d8c2..8591e2d 100644 --- a/repositories/extensions/googleSheets/package-lock.json +++ b/repositories/extensions/googleSheets/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "license": "ISC", "dependencies": { - "@query-curve/query": "^0.1.1" + "@query-curve/query": "^1.0.1" }, "devDependencies": { "@types/node": "^18.16.3", @@ -372,9 +372,9 @@ } }, "node_modules/@query-curve/query": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@query-curve/query/-/query-0.1.1.tgz", - "integrity": "sha512-32INFVSzsUQf253aMJnwz5L+wSsB7v+Xd2PExEgQWWxukxhfYW9a4Nt54GjRXT6+qOpirDVM5hNSpDv/zWtnjw==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@query-curve/query/-/query-1.0.1.tgz", + "integrity": "sha512-Ba1RGaMoVpakis55joDiX6dB/79XQ0Rf/wCH25vVUg3p/iEcfFLRudATyLXsxxuDl5xN1r4Qz5DyLO/cdLmprw==" }, "node_modules/@types/node": { "version": "18.19.22", diff --git a/repositories/extensions/googleSheets/package.json b/repositories/extensions/googleSheets/package.json index e7fd0da..1fa3cbe 100644 --- a/repositories/extensions/googleSheets/package.json +++ b/repositories/extensions/googleSheets/package.json @@ -21,6 +21,6 @@ "dist" ], "dependencies": { - "@query-curve/query": "^0.1.1" + "@query-curve/query": "^1.0.1" } } diff --git a/repositories/extensions/googleSheets/usable/index.js b/repositories/extensions/googleSheets/usable/index.js new file mode 100644 index 0000000..b3a985c --- /dev/null +++ b/repositories/extensions/googleSheets/usable/index.js @@ -0,0 +1,158 @@ +// This function is called when the spreadsheet is opened +function onOpen(e) { + var ui = SpreadsheetApp.getUi(); + ui.createMenu('QueryCurve') + .addItem('Help', 'showHelp') + .addToUi(); +} + +// This function is called when the add-on is installed +function onInstall(e) { + onOpen(e); +} + +// Function to show help dialog +function showHelp() { + var ui = SpreadsheetApp.getUi(); + ui.alert('To use the custom function, enter =QUERYCURVE(x, curve) in a cell, where x is the input value, and "curve" is the encoded curve from https://querycurve.com.'); +} + +/** + * Returns the value of a point on a curve generated at https://querycurve.com + * @param {*} value The x value to query for a corresponding y value along the curve + * @param {*} curve The encoded curve from https://querycurve.com to query + * @returns The y value corresponding to the x value on the curve + */ +function QUERYCURVE(value, curve) { + return QUERYCURVE_EXP(value, curve); +} + +const QUERYCURVE_EXP = (() => { + var BASE_62_CHAR_SET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + function fromBase62(str) { + return str.split("").reverse().reduce((prev, curr, i) => { + return prev + BASE_62_CHAR_SET.indexOf(curr) * Math.pow(62, i); + }, 0); + } + var ENCODING_SCALE_FACTOR = Math.pow(10, 7); + function decode(chain) { + const matches = chain.replace(/^/, "-").match(/--?[0-9A-Za-z]+/g) || []; + return matches.map((link) => { + const isNegative = link.startsWith("--"); + const transformed = fromBase62(link.replace(/^-+/, "")); + return (isNegative ? -transformed : transformed) / ENCODING_SCALE_FACTOR; + }); + } + function queryCurve2(curve, scaledX) { + const first = 4; + const last = curve.length - 1; + if (!(curve[0] && curve[1])) + throw new Error("Scale factors cannot be 0"); + const x = scaledX / curve[0] - curve[2]; + if (x < curve[first] || x > curve[last - 1]) + return null; + for (let i = first; i < curve.length; i += 6) { + if (curve[i] === x) + return toExternalCoordinate(curve[i + 1]); + } + let segmentStartIndex; + for (let i = first; i < curve.length - 7; i += 6) { + const startPoint = [curve[i], curve[i + 1]]; + const endPoint = [curve[i + 6], curve[i + 7]]; + if (x >= startPoint[0] && x <= endPoint[0]) { + segmentStartIndex = i; + break; + } + } + const segment = curve.slice(segmentStartIndex, segmentStartIndex + 8); + for (let attempts = 0; attempts < 10; attempts++) { + const tweak = 1e-4 * attempts; + let t = getTAtX(segment, x >= 1 ? x - tweak : x + tweak); + if (t === null) + t = getTAtXAlternative(segment, x >= 1 ? x - tweak : x + tweak); + if (t === null) + continue; + const point = getPointOnCurveAtT(segment, t); + const y = Math.abs(point[1]) < 1e-15 ? 0 : point[1]; + return toExternalCoordinate(y); + } + throw new Error("Failed to find y for x on curve"); + function toExternalCoordinate(value) { + const scaled = (value + curve[3]) * curve[1]; + return Object.is(scaled, -0) ? 0 : scaled; + } + } + function queryEncodedCurve2(encodedChain, scaledX) { + const chain = decode(encodedChain); + return queryCurve2(chain, scaledX); + } + function getEncodedCurveQueryFunction(encodedChain) { + const decodedChain = decode(encodedChain); + return (scaledX) => queryCurve2(decodedChain, scaledX); + } + function getPointOnCurveAtT(segment, t) { + const mt = 1 - t; + const mt2 = mt * mt; + const t2 = t * t; + const a = mt2 * mt; + const b = mt2 * t * 3; + const c = mt * t2 * 3; + const d = t * t2; + const x = a * segment[0] + b * segment[2] + c * segment[4] + d * segment[6]; + const y = a * segment[1] + b * segment[3] + c * segment[5] + d * segment[7]; + return [x, y]; + } + function getDerivativeAtT(segment, t) { + const mt = 1 - t; + const t2 = t * t; + const a = -3 * mt * mt; + const b = 3 * mt * (mt - 2 * t); + const c = 3 * t * (2 * mt - t); + const d = 3 * t2; + const x = a * segment[0] + b * segment[2] + c * segment[4] + d * segment[6]; + const y = a * segment[1] + b * segment[3] + c * segment[5] + d * segment[7]; + return [x, y]; + } + function getTAtX(segment, x) { + let t = 0.5; + let xAtT, xDerivativeAtT, xDifference, iterationCount = 0; + do { + const pointAtT = getPointOnCurveAtT(segment, t); + const derivativeAtT = getDerivativeAtT(segment, t); + xAtT = pointAtT[0]; + xDerivativeAtT = derivativeAtT[0]; + xDifference = x - xAtT; + if (Math.abs(xDerivativeAtT) > 1e-6) { + t += xDifference / xDerivativeAtT; + } + t = Math.max(Math.min(t, 1), 0); + iterationCount++; + if (iterationCount > 15) { + return null; + } + } while (Math.abs(xDifference) > 1e-6); + return t; + } + function getTAtXAlternative(segment, x, tolerance = 1e-6, maxIterations = 100) { + let a = 0; + let b = 1; + let t = (a + b) / 2; + for (let i = 0; i < maxIterations; i++) { + t = (a + b) / 2; + const xAtT = getPointOnCurveAtT(segment, t)[0]; + if (Math.abs(xAtT - x) <= tolerance) { + return t; + } + if (xAtT > x !== getPointOnCurveAtT(segment, a)[0] > x) { + b = t; + } else { + a = t; + } + } + return null; + } + + return function QUERYCURVE(value, curve) { + return queryEncodedCurve2(curve, typeof value === "string" ? parseFloat(value) : value); + }; +})(); diff --git a/repositories/query/typescript/README.md b/repositories/query/typescript/README.md index e2a981c..f832ca2 100644 --- a/repositories/query/typescript/README.md +++ b/repositories/query/typescript/README.md @@ -1,3 +1,54 @@ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ralusek/query-curve/blob/master/LICENSE) -[![npm version](https://img.shields.io/npm/v/query-curve.svg?style=flat)](https://www.npmjs.com/package/query-curve) +[![npm version](https://img.shields.io/npm/v/@query-curve/query.svg?style=flat)](https://www.npmjs.com/package/@query-curve/query) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/ralusek/query-curve/blob/master/LICENSE) + +# QueryCurve +This tool allows you to invoke queries against a curve you've laid out at [https://querycurve.com](https://querycurve.com/) + +Once you have a curve in the shape you'd like: +![Example curve from QueryCurve.com](https://querycurve.com/example_d.png) + +You'll get a resulting encoded curve that'll look like this: +`2BLnMW-2BLnMW--KyjA--KyjA-0-KyjA-CaR6-XZAG-KyjA-TN1E-KyjA-KyjA-KyjA-CaR6-TN1E-8OI4-fxSK-KyjA` + +## Time to query! + +### Installation from npm +```bash +$ npm install --save @query-curve/query +``` + +### Usage + +```typescript +import { getEncodedCurveQueryFunction, queryEncodedCurve } from '@query-curve/query'; + +queryEncodedCurve('5SNUPI-8nlt2n2-0-0-0-fxSK-3yGp-fn3A-TzAp-e6zY-bau8-PAsC-dGxk-LXPh-f3xT-9cbF-fxSK-0', 0); +``` + +#### Querying with a dynamically loaded curve +If you are pulling your curve from a db or otherwise need it to be dynamic: +```typescript +const dynamicallyLoadedCurve = 'fxSK-fxSK-0-0-0-0-KyjA-0-KyjA-fxSK-fxSK-fxSK'; // assume this was loaded from db +const myXValue = 0.35; + +// Gets the corresponding y value along the curve for a given x +const result = queryEncodedCurve(dynamicallyLoadedCurve, myXValue); +``` +Note: While decoding the curve is fast, repeatedly querying against the same curve can be optimized by preloading the curve. +If you anticipate multiple queries against the same curve, consider using: + +#### Querying with a preloaded or reused curve +If the curve you're using will be used to facilitate multiple queries, this alternative for querying will +bypass the need to decode the curve on every query. + +```typescript +import { getEncodedCurveQueryFunction } from '@query-curve/query'; +const fixedCurve = 'fxSK-fxSK-0-0-0-0-KyjA-0-KyjA-fxSK-fxSK-fxSK'; +// Returns a function with a reference to the decoded curve +const queryMyCurve = getEncodedCurveQueryFunction(fixed_curve) ; + +queryMyCurve(0); +queryMyCurve(0.5); +queryMyCurve(0.37); +``` diff --git a/repositories/query/typescript/package.json b/repositories/query/typescript/package.json index f9c72ef..e1f65ab 100644 --- a/repositories/query/typescript/package.json +++ b/repositories/query/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@query-curve/query", - "version": "1.0.0", + "version": "1.0.1", "description": "@query-curve utility for querying for a value along a cubic bezier curve.", "main": "dist/index.js", "types": "dist/index.d.ts",