Repository: blacksmithgu/obsidian-dataview (ts)

Issue:
date function fails for null values when format is given.
### What happened?

I try to have some timetracking in my obsidian. Therefore, I create notes with frontmatter tags "start" and "end" and a link back to my daily note.
In said daily note I have a dataview that is supposed to show all "timetracking entries" (eg notes).
I then tried to make dataview automatically calculate the duration of each task, and my query resulted in an error:
`No implementation of 'date' found for arguments: null, string`
I found out why this happens - because as long as a task is running, the "end" frontmatter tag is not set. I simplified the problem to the provided DQL.
The line `  , date(end, "yyyy")` breaks the query, 
![image](https://github.com/blacksmithgu/obsidian-dataview/assets/142982206/61f20956-a089-46cc-9ee3-6ee2be4cc802)
the line `  , date(end)` is fine
![image](https://github.com/blacksmithgu/obsidian-dataview/assets/142982206/a22ac252-2174-4149-a3b4-eb624ef24484)


So, apparently, `date(null)` is fine, `date(null, "some format")` is not.

### DQL

```dataview
TABLE WITHOUT id
  link(file.name, task) as "Task"
  , end
  , date(end)
  , date(end, "yyyy")
FROM [[2024-03-01]]
SORT file.name
```

### JS

_No response_

### Dataview Version

0.5.64

### Obsidian Version

1.5.8

### OS

Linux

Pull request description:
bug 2253 - Allow for the date to be null
Fixed #2253 allowing the date to null, returning null, instead of breaking the query.

(Also ran `npm run format` which picked up some correction of formatting in render.ts)

Patch:
diff --git a/docs/docs/reference/functions.md b/docs/docs/reference/functions.md
index 310f880..ae3ff61 100644
--- a/docs/docs/reference/functions.md
+++ b/docs/docs/reference/functions.md
@@ -700,6 +700,25 @@ choice(false, "yes", "no") = "no"
 choice(x > 4, y, z) = y if x > 4, else z
 ```
 
+### `hash(seed, [text], [variant])`
+
+Generate a hash based on the `seed`, and the optional extra `text` or a variant `number`. The function
+generates a fixed number based on the combination of these parameters, which can be used to randomise 
+the sort order of files or lists/tasks. If you choose a `seed` based on a date, i.e. "2024-03-17",
+or another timestamp, i.e. "2024-03-17 19:13", you can make the "randomness" be fixed
+related to that timestamp. `variant` is a number, which in some cases is needed to make the combination of
+`text` and `variant` become unique.
+
+```js
+hash(dateformat(date(today), "YYYY-MM-DD"), file.name) = ... A unique value for a given date in time
+hash(dateformat(date(today), "YYYY-MM-DD"), file.name, position.start.line) = ... A unique "random" value in a TASK query
+```
+
+This function can be used in a `SORT` statement to randomise the order. If you're using a `TASK` query, 
+since the file name could be the same for multiple tasks, you can add some number like the starting line
+number (as shown above) to make it a unique combination. If using something like `FLATTEN file.lists as item`, 
+the similar addition would be to do `item.position.start.line` as the last parameter.
+
 ### `striptime(date)`
 
 Strip the time component of a date, leaving only the year, month, and day. Good for date comparisons if you don't care
diff --git a/src/expression/functions.ts b/src/expression/functions.ts
index 7bd5c5d..b3ebd0e 100644
--- a/src/expression/functions.ts
+++ b/src/expression/functions.ts
@@ -9,6 +9,7 @@ import { Fields } from "./field";
 import { EXPRESSION } from "./parse";
 import { escapeRegex } from "util/normalize";
 import { DataArray } from "api/data-array";
+import { cyrb53 } from "util/hash";
 
 /**
  * A function implementation which takes in a function context and a variable number of arguments. Throws an error if an
@@ -292,6 +293,7 @@ export namespace DefaultFunctions {
                 }
             }
         })
+        .add2("null", "string", () => null)
         .add1("null", () => null)
         .vectorize(1, [0])
         .build();
@@ -704,6 +706,18 @@ export namespace DefaultFunctions {
         .vectorize(3, [0])
         .build();
 
+    export const hash = new FunctionBuilder("hash")
+        .add2("string", "number", (seed, variant) => {
+            return cyrb53(seed, variant);
+        })
+        .add2("string", "string", (seed, text) => {
+            return cyrb53(seed + text);
+        })
+        .add3("string", "string", "number", (seed, text, variant) => {
+            return cyrb53(seed + text, variant);
+        })
+        .build();
+
     export const reduce = new FunctionBuilder("reduce")
         .add2("array", "string", (lis, op, context) => {
             if (lis.length == 0) return null;
@@ -924,5 +938,6 @@ export const DEFAULT_FUNCTIONS: Record<string, FunctionImpl> = {
     default: DefaultFunctions.fdefault,
     ldefault: DefaultFunctions.ldefault,
     choice: DefaultFunctions.choice,
+    hash: DefaultFunctions.hash,
     meta: DefaultFunctions.meta,
 };
diff --git a/src/ui/render.ts b/src/ui/render.ts
index 8920177..3e1667a 100644
--- a/src/ui/render.ts
+++ b/src/ui/render.ts
@@ -88,7 +88,14 @@ export async function renderValue(
     }
 
     if (Values.isNull(field)) {
-        await renderCompactMarkdown(app, settings.renderNullAs, container, originFile, component, isInlineFieldLivePreview);
+        await renderCompactMarkdown(
+            app,
+            settings.renderNullAs,
+            container,
+            originFile,
+            component,
+            isInlineFieldLivePreview
+        );
     } else if (Values.isDate(field)) {
         container.appendText(renderMinimalDate(field, settings, currentLocale()));
     } else if (Values.isDuration(field)) {
diff --git a/src/util/hash.ts b/src/util/hash.ts
new file mode 100644
index 0000000..83ab5fd
--- /dev/null
+++ b/src/util/hash.ts
@@ -0,0 +1,22 @@
+// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.
+// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.
+// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
+// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
+// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
+
+export function cyrb53(str: string, seed: number = 0): number {
+    let h1 = 0xdeadbeef ^ seed,
+        h2 = 0x41c6ce57 ^ seed;
+    for (let i = 0, ch; i < str.length; i++) {
+        ch = str.charCodeAt(i);
+        h1 = Math.imul(h1 ^ ch, 2654435761);
+        h2 = Math.imul(h2 ^ ch, 1597334677);
+    }
+    h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
+    h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
+    h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
+    h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
+    // For a full 64-bit value we could return
+    //  [h2>>>0, h1>>>0]
+    return 4294967296 * (2097151 & h2) + (h1 >>> 0); // ;
+}


Test patch:
diff --git a/src/test/function/functions.test.ts b/src/test/function/functions.test.ts
index fc35851..288ce35 100644
--- a/src/test/function/functions.test.ts
+++ b/src/test/function/functions.test.ts
@@ -124,6 +124,13 @@ test("Evaluate choose()", () => {
     expect(parseEval("choice(false, 1, 2)")).toEqual(2);
 });
 
+test("Evaulate hash()", () => {
+    expect(DefaultFunctions.hash(simpleContext(), "2024-03-17", "")).toEqual(3259376374957153);
+    expect(DefaultFunctions.hash(simpleContext(), "2024-03-17", 2)).toEqual(271608741894590);
+    expect(DefaultFunctions.hash(simpleContext(), "2024-03-17", "Home")).toEqual(3041844187830523);
+    expect(DefaultFunctions.hash(simpleContext(), "2024-03-17", "note a1", 21)).toEqual(1143088188331616);
+});
+
 // <-- extract() -->
 
 test("Evaluate 1 field extract()", () => {
@@ -157,4 +164,5 @@ test("Evaluate date()", () => {
     expect(parseEval('date("210313", "yyMMdd")')).toEqual(DateTime.fromObject({ year: 2021, month: 3, day: 13 }));
     expect(parseEval('date("946778645012","x")')).toEqual(DateTime.fromMillis(946778645012));
     expect(parseEval('date("946778645","X")')).toEqual(DateTime.fromMillis(946778645000));
+    expect(DefaultFunctions.date(simpleContext(), null, "MM/dd/yyyy")).toEqual(null);
 });
diff --git a/test-vault/.obsidian/community-plugins.json b/test-vault/.obsidian/community-plugins.json
index f51d89b..a832eab 100644
--- a/test-vault/.obsidian/community-plugins.json
+++ b/test-vault/.obsidian/community-plugins.json
@@ -1,4 +1,4 @@
 [
-  "dataview",
-  "hot-reload"
+  "hot-reload",
+  "dataview"
 ]
\ No newline at end of file