Hi folks!
Here’s my take at it, just for fun…
Specs:
- Only filter syntax, no wikitext, because… why not?
- We want a function with 2 parameters considered as strings (empty values are Ok)
- When both parameters are identical, the function returns nothing (even if both parameters are empty)
- Else the function returns the index of the first different character, “1-based”.
Here’s the code:
\function str.secondIsLonger(s1, s2) [<s1>length[]] :filter[<s2>length[]compare::gt<currentTiddler>then<currentTiddler>]
\function str.firstDiffIndex(str1, str2) [<str1>split[]] :map:[<str2>split[]zth<index>else[]!match<currentTiddler>then<index>] :filter[<currentTiddler>!is[blank]] [str.secondIsLonger<str1>,<str2>] :else[[-1]] +[minall[]!match[-1]add[1]]
---
Examples:
---
Should be "same":
{{{ [str.firstDiffIndex[abcdefgh],[abcdefgh]] :else[[same]] }}}
---
Should be "same":
{{{ [str.firstDiffIndex[],[]] :else[[same]] }}}
---
Should be "4":
{{{ [str.firstDiffIndex[abcdefgh],[abcZdefgh]] :else[[same]] }}}
---
Should be "9":
{{{ [str.firstDiffIndex[abcdefgh],[abcdefghZ]] :else[[same]] }}}
---
Should be "9":
{{{ [str.firstDiffIndex[abcdefghZ],[abcdefgh]] :else[[same]] }}}
---
Should be "1":
{{{ [str.firstDiffIndex[],[abcdefgh]] :else[[same]] }}}
---
Should be "1":
{{{ [str.firstDiffIndex[abcdefgh],[]] :else[[same]] }}}
Notes:
- The
str.firstDiffIndex
function’s first filter run splits every char from its first parameter str1 ([<str1>split[]]
)
- In the second filter run, the second parameter (str2) characters are split and relevant char is chosen using the
index
variable of the filter run (<str2>split[]zth<index>
). When str2 is shorter than str1, the result may be empty, in which case it’s replaced by an empty char (else[]
) in order to induce a difference.
- At this point the nth char of str2 (or an empty string), is compared with the current str1 character and if they don’t match the result is the current index (
!match<currentTiddler>then<index>
).
- Then a
:filter
filter run removes empty values left by the :map
filter run – :map
result always has at least as many items as its input – (:filter[<currentTiddler>!is[blank]]
).
- Now we need to address the special case of “abc” vs “abcZ” ie str2 is identical to str1 but with added content. In this case the result should be the length of str1 +1. That’s the purpose of
str.secondIsLonger
function.
- Function
str.secondIsLonger
compares lengths of its parameters and returns the length of the first one only if the second one is longer. It leverages one great power of functions, which can be composed of several filter runs but behave like a filter operator when used. Filter syntax doesn’t allow things like <str2>length[]compare::gt[ <str1>length[] ]
, so here 2 filter runs are used.
- As a result of all this, we now have a list maybe containing some differences indexes, then maybe the length of str1, or… nothing at all when both strings are the same.
- The lowest value in this list is the first difference, so
+[minall[]]
should do the trick, but… when given no input, the result of +[minall[]]
is “Infinity” (litteraly)! To avoid this behavior, a special value is added when the list is empty (:else[[-1]]
) just before choosing (+[minall[]
) then excluding it back again (!match[-1]
).
- Lastly, any index value is “0-based” but we want a more human-readable “1-based” value, so we add 1 (
add[1]
), hence the code of the 2 last filter runs::else[[-1]] +[minall[]!match[-1]add[1]]
.
Please note that all this code is quite expensive, so it’s not ready for production use: for each character of the first parameter, the second parameter is split even if a difference has already been found (or worse, if there’s no more character in str2 to compare to)! Anyway, it’s been a lot of fun!
Fred