Repository: blacksmithgu/obsidian-dataview
Pull request #1584 (2022-11-19 11:58:14)

feat: add a new regextest function and deprecate regexmatch
## Motivation

The `regexmatch` function only matches exact strings because it adds the `^` and `$` special characters to **every** pattern.

This results in the following test failing:

```js
test("not exact", () => expect(parseEval('regexmatch("what", "what are you doing?")')).toEqual(true));
```

## Solution

<details>
<summary>First solution</summary>

Stop adding the delimiters (`^` and `$`) to every pattern:

```typescript
 export const regexmatch = new FunctionBuilder("regexmatch")
        .add2("string", "string", (pattern: string, field: string) => {
            if (!pattern.startsWith("^") && !pattern.endsWith("$")) pattern = "^" + pattern + "$";
//          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            return !!field.match(pattern);
        })
```

</details>

Added a new function called `regextest` that matches javascript's [test](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) function.

Also, deprecated `regexmatch` to prepare for a major-version-change removal and update the docs to make clear what the function does.

## Considerations

~This is probably a breaking change since people might be expecting to have the incorrect behavior, but I think a patch is ok.~

##### Cheers

This is an awesome plugin! Happy to contribute more to it.

blacksmithgu: I agree with the value of not always doing `$` and `^` for every match, but breaking changes are really rough - would adding a new method work instead? Such as `regexfind` or `regexinmatch` or something similar.
alexfertel: > Would adding a new method work instead?

Yes, @blacksmithgu ! That makes sense. However, I would suggest updating the docs for `regexmatch` and deprecating it to avoid people relying on its behavior in the future and potentially decommissioning it in a major version change.

> Such as `regexfind` or `regexinmatch` or something similar.

There is another small issue with `regexmatch`, its name. I think it should be `regextest` since the matches are discarded by `!!field.match(pattern);` and it is more close in nature to [test](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test).

Therefore, should we use `regextest`? I'll update the PR later today.

alexfertel: @blacksmithgu let me know how you feel about the latest changes!

Patch:
diff --git a/docs/docs/reference/expressions.md b/docs/docs/reference/expressions.md
index 3e91a85..2f548f1 100644
--- a/docs/docs/reference/expressions.md
+++ b/docs/docs/reference/expressions.md
@@ -39,6 +39,10 @@ a != b              (check if a does not equal b)
 a <= b              (check if a is less than or equal to b)
 a >= b              (check if a is greater than or equal to b)
 
+# Strings
+
+a + b               (string concatenation)
+
 # Special Operations
 [[Link]].value      (fetch `value` from page `Link`)
 ```
@@ -100,7 +104,7 @@ See the [note in the FAQ](../resources/faq.md#how-do-i-use-fields-with-the-same-
 
 Dataview supports various functions for manipulating data, which are described in full in the [functions
 documentation](../functions). They have the general syntax `function(arg1, arg2, ...)` - i.e., `lower("yes")` or
-`regexmatch("text", ".+")`.
+`regextest("text", ".+")`.
 
 ### Lambdas
 
diff --git a/docs/docs/reference/functions.md b/docs/docs/reference/functions.md
index ada37a0..1afe3d3 100644
--- a/docs/docs/reference/functions.md
+++ b/docs/docs/reference/functions.md
@@ -456,14 +456,28 @@ map(["yes", "no"], (x) => x + "?") = ["yes?", "no?"]
 
 ## String Operations
 
-### `regexmatch(pattern, string)`
+### `regextest(pattern, string)`
 
 Checks if the given string matches the given pattern (using the JavaScript regex engine).
 
+```
+regextest("\w+", "hello") = true
+regextest(".", "a") = true
+regextest("yes|no", "maybe") = false
+regextest("what", "what's up dog?") = true
+```
+
+### `regexmatch(pattern, string)` (DEPRECATED)
+
+Checks if the given string matches the given pattern. Note: the pattern gets a `^` 
+prepended and a `$` appended to it, which means it will only match if the match starts at
+the beginning and ends at the end of the string. (using the JavaScript regex engine).
+
 ```
 regexmatch("\w+", "hello") = true
 regexmatch(".", "a") = true
 regexmatch("yes|no", "maybe") = false
+regexmatch("what", "what's up dog?") = false
 ```
 
 ### `regexreplace(string, pattern, replacement)`
diff --git a/src/data-model/markdown.ts b/src/data-model/markdown.ts
index e0c6af7..eb9ebff 100644
--- a/src/data-model/markdown.ts
+++ b/src/data-model/markdown.ts
@@ -273,7 +273,7 @@ export class ListItem {
             header: this.section, // @deprecated, use 'item.section' instead.
         };
 
-        if (this.parent) result.parent = this.parent;
+        if (this.parent || this.parent === 0) result.parent = this.parent;
         if (this.blockId) result.blockId = this.blockId;
 
         addFields(this.fields, result);
diff --git a/src/expression/functions.ts b/src/expression/functions.ts
index 0a0b294..ae9c1f2 100644
--- a/src/expression/functions.ts
+++ b/src/expression/functions.ts
@@ -504,6 +504,13 @@ export namespace DefaultFunctions {
         .add1("*", e => e)
         .build();
 
+    export const regextest = new FunctionBuilder("regextest")
+        .add2("string", "string", (pattern: string, field: string) => RegExp(pattern).test(field))
+        .add2("null", "*", (_n, _a) => false)
+        .add2("*", "null", (_a, _n) => false)
+        .vectorize(2, [0, 1])
+        .build();
+
     export const regexmatch = new FunctionBuilder("regexmatch")
         .add2("string", "string", (pattern: string, field: string) => {
             if (!pattern.startsWith("^") && !pattern.endsWith("$")) pattern = "^" + pattern + "$";
@@ -784,6 +791,7 @@ export const DEFAULT_FUNCTIONS: Record<string, FunctionImpl> = {
 
     // String operations.
     regexreplace: DefaultFunctions.regexreplace,
+    regextest: DefaultFunctions.regextest,
     regexmatch: DefaultFunctions.regexmatch,
     replace: DefaultFunctions.replace,
     lower: DefaultFunctions.lower,


Test patch:
diff --git a/src/test/function/aggregation.test.ts b/src/test/function/aggregation.test.ts
index 5e96733..4da1986 100644
--- a/src/test/function/aggregation.test.ts
+++ b/src/test/function/aggregation.test.ts
@@ -65,6 +65,10 @@ describe("all()", () => {
         expectEvals('all(regexmatch("a+", list("a", "aaaa")))', true);
         expectEvals('all(regexmatch("a+", list("a", "aaab")))', false);
         expectEvals('any(regexmatch("a+", list("a", "aaab")))', true);
+
+        expectEvals('all(regextest("a+", list("a", "aaaa")))', true);
+        expectEvals('all(regextest("a+", list("a", "aaab")))', true);
+        expectEvals('any(regextest("a+", list("a", "aaab")))', true);
     });
 });
 
diff --git a/src/test/function/string.test.ts b/src/test/function/string.test.ts
index d926cdb..c974caa 100644
--- a/src/test/function/string.test.ts
+++ b/src/test/function/string.test.ts
@@ -20,6 +20,19 @@ describe("containsword()", () => {
     test("case insensitive 2", () => expect(parseEval('containsword("Hello there, chap!", "HELLO")')).toEqual(true));
 });
 
+// <-- regextest() -->
+
+describe("regextest()", () => {
+    test(".+", () => expect(parseEval('regextest(".+", "stuff")')).toEqual(true));
+    test(".+ empty", () => expect(parseEval('regextest(".+", "")')).toEqual(false));
+    test(".* empty", () => expect(parseEval('regextest(".*", "")')).toEqual(true));
+    test("word", () => expect(parseEval('regextest("\\w+", "m3me")')).toEqual(true));
+    test("whitespace", () => expect(parseEval('regextest("\\s+", "  ")')).toEqual(true));
+    test("exact", () => expect(parseEval('regextest("what", "what")')).toEqual(true));
+    test("not exact", () => expect(parseEval('regextest("what", "what are you doing?")')).toEqual(true));
+    test("start & end", () => expect(parseEval('regextest("^what$", "what")')).toEqual(true));
+});
+
 // <-- regexmatch() -->
 
 describe("regexmatch()", () => {