Skip to content

Commit 2208dbe

Browse files
committed
Feature srandom - A seeded random number
A function generating seeded random numbers, allowing for sorting lists in a random, but predictively fashion.
1 parent e4a6cab commit 2208dbe

File tree

6 files changed

+118
-2
lines changed

6 files changed

+118
-2
lines changed

docs/docs/reference/functions.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,17 @@ maxby([1, 2, 3], (k) => 0 - k) => 1
272272
maxby(this.file.tasks, (k) => k.due) => (latest due)
273273
```
274274

275-
--
275+
### `srandom([number|text])`
276+
277+
Returns a seeded random number based on either a `number` or a `text`. Repeated calls to `srandom()` with the same seed, gives the same sequence of random numbers. This is useful in a query when combined with sorting a list on this sequence, and then doing a `LIMIT` on that sequence, to get a given set of random items for that seed.
278+
279+
In other words, if used with a date, you can sort your list/quotes/... and get a random element from that list, and it'll change when your date string changes. This way you can get a random item each day, each hour, etc based on what you use as the seed.
280+
281+
```js
282+
srandom(12345) x3 = 0.6462731098290533, 0.5638589910231531, 0.35898207360878587
283+
srandom("2024-02-28") x3 = 0.44641065830364823, 0.988620902877301, 0.01667086035013199
284+
```
285+
---
276286

277287
## Objects, Arrays, and String Operations
278288

docs/docs/resources/examples.md

+15
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,18 @@ List all files which have a date in their title (of the form `yyyy-mm-dd`), and
8383
=== "Output"
8484
- [2021-08-07](#): August 07, 2021
8585
- [2020-08-10](#): August 10, 2020
86+
87+
---
88+
89+
Get three random links from your vault, which changes every day, but are consistent throughout the day. Similar queries can also be used to get random quotes, or other random items to your liking.
90+
91+
=== "Query"
92+
```sql
93+
LIST
94+
FLATTEN srandom(dateformat(date(today), "yyyy-MM-dd")) as randomValue
95+
SORT randomValue
96+
LIMIT 3
97+
```
98+
=== "Output"
99+
100+
Three random links from your vault, where on any given day the three links are consistently the same. The only thing capable of changing it, is changing the source list from where you're pulling the random items.

src/expression/functions.ts

+11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Fields } from "./field";
99
import { EXPRESSION } from "./parse";
1010
import { escapeRegex } from "util/normalize";
1111
import { DataArray } from "api/data-array";
12+
import { executeSrandom } from "util/srandom";
1213

1314
/**
1415
* A function implementation which takes in a function context and a variable number of arguments. Throws an error if an
@@ -441,6 +442,15 @@ export namespace DefaultFunctions {
441442
.add2("null", "function", (_arr, _func, _ctx) => null)
442443
.build();
443444

445+
export const srandom: FunctionImpl = new FunctionBuilder("srandom")
446+
.add1("number", (seed, ctx) => {
447+
return executeSrandom(seed.toString(), ctx);
448+
})
449+
.add1("string", (seed, ctx) => {
450+
return executeSrandom(seed, ctx);
451+
})
452+
.build();
453+
444454
export const striptime = new FunctionBuilder("striptime")
445455
.add1("date", d => DateTime.fromObject({ year: d.year, month: d.month, day: d.day }))
446456
.add1("null", _n => null)
@@ -861,6 +871,7 @@ export const DEFAULT_FUNCTIONS: Record<string, FunctionImpl> = {
861871
max: DefaultFunctions.max,
862872
minby: DefaultFunctions.minby,
863873
maxby: DefaultFunctions.maxby,
874+
srandom: DefaultFunctions.srandom,
864875

865876
// String operations.
866877
regexreplace: DefaultFunctions.regexreplace,

src/query/engine.ts

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Field, Fields } from "expression/field";
1111
import { QuerySettings } from "settings";
1212
import { DateTime } from "luxon";
1313
import { SListItem } from "data-model/serialized/markdown";
14+
import { randomUUID } from "crypto";
1415

1516
function iden<T>(x: T): T {
1617
return x;
@@ -297,6 +298,7 @@ export async function executeList(
297298
// Extract information about the origin page to add to the root context.
298299
let rootContext = new Context(defaultLinkHandler(index, origin), settings, {
299300
this: index.pages.get(origin)?.serialize(index) ?? {},
301+
queryUUID: randomUUID(),
300302
});
301303

302304
let targetField = (query.header as ListQuery).format;
@@ -339,6 +341,7 @@ export async function executeTable(
339341
// Extract information about the origin page to add to the root context.
340342
let rootContext = new Context(defaultLinkHandler(index, origin), settings, {
341343
this: index.pages.get(origin)?.serialize(index) ?? {},
344+
queryUUID: randomUUID(),
342345
});
343346

344347
let targetFields = (query.header as TableQuery).fields;
@@ -419,6 +422,7 @@ export async function executeTask(
419422
// Extract information about the origin page to add to the root context.
420423
let rootContext = new Context(defaultLinkHandler(index, origin), settings, {
421424
this: index.pages.get(origin)?.serialize(index) ?? {},
425+
queryUUID: randomUUID(),
422426
});
423427

424428
return executeCore(incomingTasks, rootContext, query.operations).map(core => {
@@ -441,6 +445,7 @@ export function executeInline(
441445
): Result<Literal, string> {
442446
return new Context(defaultLinkHandler(index, origin), settings, {
443447
this: index.pages.get(origin)?.serialize(index) ?? {},
448+
queryUUID: randomUUID(),
444449
}).evaluate(field);
445450
}
446451

@@ -481,6 +486,7 @@ export async function executeCalendar(
481486
// Extract information about the origin page to add to the root context.
482487
let rootContext = new Context(defaultLinkHandler(index, origin), settings, {
483488
this: index.pages.get(origin)?.serialize(index) ?? {},
489+
queryUUID: randomUUID(),
484490
});
485491

486492
let targetField = (query.header as CalendarQuery).field.field;

src/test/function/aggregation.test.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { expectEvals } from "test/common";
1+
import { EXPRESSION } from "expression/parse";
2+
import { expectEvals, simpleContext } from "test/common";
23

34
describe("map()", () => {
45
test("empty list", () => expectEvals("map([], (k) => 6)", []));
@@ -39,6 +40,20 @@ describe("maxby()", () => {
3940
test("multiple", () => expectEvals("maxby([1, 2, 3], (k) => 0 - k)", 1));
4041
});
4142

43+
describe("srandom()", () => {
44+
let context = simpleContext().set("queryUUID", "abcdef-1234");
45+
test("12345", () =>
46+
expect(
47+
context.tryEvaluate(EXPRESSION.field.tryParse("list(srandom(12345), srandom(12345), srandom(12345))"))
48+
).toEqual([0.6462731098290533, 0.5638589910231531, 0.35898207360878587]));
49+
test('"2024-02-28"', () =>
50+
expect(
51+
context.tryEvaluate(
52+
EXPRESSION.field.tryParse('list(srandom("2024-02-28"), srandom("2024-02-28"), srandom("2024-02-28"))')
53+
)
54+
).toEqual([0.44641065830364823, 0.988620902877301, 0.01667086035013199]));
55+
});
56+
4257
describe("sum()", () => {
4358
test("number list", () => expectEvals("sum([2, 3, 1])", 6));
4459
test("string list", () => expectEvals('sum(["a", "b", "c"])', "abc"));

src/util/srandom.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Context } from "expression/context";
2+
3+
/* This seeded random generator is based upon this stackoverflow answer:
4+
* https://stackoverflow.com/a/47593316
5+
*
6+
* And the sfc32() and cyrb128() functions copied directrly from it,
7+
* so thanks to https://stackoverflow.com/users/815680/bryc
8+
*/
9+
10+
function sfc32(a: number, b: number, c: number, d: number): () => number {
11+
return function (): number {
12+
a |= 0;
13+
b |= 0;
14+
c |= 0;
15+
d |= 0;
16+
var t = (((a + b) | 0) + d) | 0;
17+
d = (d + 1) | 0;
18+
a = b ^ (b >>> 9);
19+
b = (c + (c << 3)) | 0;
20+
c = (c << 21) | (c >>> 11);
21+
c = (c + t) | 0;
22+
return (t >>> 0) / 4294967296;
23+
};
24+
}
25+
26+
function cyrb128(str: string): number[] {
27+
let h1 = 1779033703,
28+
h2 = 3144134277,
29+
h3 = 1013904242,
30+
h4 = 2773480762;
31+
for (let i = 0, k; i < str.length; i++) {
32+
k = str.charCodeAt(i);
33+
h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
34+
h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
35+
h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
36+
h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
37+
}
38+
h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
39+
h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
40+
h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
41+
h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
42+
(h1 ^= h2 ^ h3 ^ h4), (h2 ^= h1), (h3 ^= h1), (h4 ^= h1);
43+
return [h1 >>> 0, h2 >>> 0, h3 >>> 0, h4 >>> 0];
44+
}
45+
46+
/* Return the unique srandom function for a given query,
47+
* using the provided seed
48+
*/
49+
export function executeSrandom(seed: string, ctx: Context): number {
50+
const internalKey = (ctx.get("queryUUID") as string) + "§" + seed;
51+
52+
// If key not present, generate new srandom function
53+
if (!ctx.globals.hasOwnProperty(internalKey)) {
54+
const [a, b, c, d] = cyrb128(seed);
55+
ctx.set(internalKey, sfc32(a, b, c, d));
56+
}
57+
58+
return (ctx.get(internalKey) as () => number)();
59+
}

0 commit comments

Comments
 (0)