1 /* String manipulation functions. */
5 inherit (builtins) length;
33 unsafeDiscardStringContext
36 /* Concatenate a list of strings.
38 Type: concatStrings :: [string] -> string
41 concatStrings ["foo" "bar"]
44 concatStrings = builtins.concatStringsSep "";
46 /* Map a function over a list and concatenate the resulting strings.
48 Type: concatMapStrings :: (a -> string) -> [a] -> string
51 concatMapStrings (x: "a" + x) ["foo" "bar"]
54 concatMapStrings = f: list: concatStrings (map f list);
56 /* Like `concatMapStrings` except that the f functions also gets the
57 position as a parameter.
59 Type: concatImapStrings :: (int -> a -> string) -> [a] -> string
62 concatImapStrings (pos: x: "${toString pos}-${x}") ["foo" "bar"]
65 concatImapStrings = f: list: concatStrings (lib.imap1 f list);
67 /* Place an element between each element of a list
69 Type: intersperse :: a -> [a] -> [a]
72 intersperse "/" ["usr" "local" "bin"]
73 => ["usr" "/" "local" "/" "bin"].
76 # Separator to add between elements
80 if list == [] || length list == 1
82 else tail (lib.concatMap (x: [separator x]) list);
84 /* Concatenate a list of strings with a separator between each element
86 Type: concatStringsSep :: string -> [string] -> string
89 concatStringsSep "/" ["usr" "local" "bin"]
92 concatStringsSep = builtins.concatStringsSep or (separator: list:
93 lib.foldl' (x: y: x + y) "" (intersperse separator list));
95 /* Maps a function over a list of strings and then concatenates the
96 result with the specified separator interspersed between
99 Type: concatMapStringsSep :: string -> (a -> string) -> [a] -> string
102 concatMapStringsSep "-" (x: toUpper x) ["foo" "bar" "baz"]
105 concatMapStringsSep =
106 # Separator to add between elements
108 # Function to map over the list
110 # List of input strings
111 list: concatStringsSep sep (map f list);
113 /* Same as `concatMapStringsSep`, but the mapping function
114 additionally receives the position of its argument.
116 Type: concatIMapStringsSep :: string -> (int -> a -> string) -> [a] -> string
119 concatImapStringsSep "-" (pos: x: toString (x / pos)) [ 6 6 6 ]
122 concatImapStringsSep =
123 # Separator to add between elements
125 # Function that receives elements and their positions
127 # List of input strings
128 list: concatStringsSep sep (lib.imap1 f list);
130 /* Construct a Unix-style, colon-separated search path consisting of
131 the given `subDir` appended to each of the given paths.
133 Type: makeSearchPath :: string -> [string] -> string
136 makeSearchPath "bin" ["/root" "/usr" "/usr/local"]
137 => "/root/bin:/usr/bin:/usr/local/bin"
138 makeSearchPath "bin" [""]
142 # Directory name to append
146 concatStringsSep ":" (map (path: path + "/" + subDir) (filter (x: x != null) paths));
148 /* Construct a Unix-style search path by appending the given
149 `subDir` to the specified `output` of each of the packages. If no
150 output by the given name is found, fallback to `.out` and then to
153 Type: string -> string -> [package] -> string
156 makeSearchPathOutput "dev" "bin" [ pkgs.openssl pkgs.zlib ]
157 => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r-dev/bin:/nix/store/wwh7mhwh269sfjkm6k5665b5kgp7jrk2-zlib-1.2.8/bin"
159 makeSearchPathOutput =
160 # Package output to use
162 # Directory name to append
165 pkgs: makeSearchPath subDir (map (lib.getOutput output) pkgs);
167 /* Construct a library search path (such as RPATH) containing the
168 libraries for a set of packages
171 makeLibraryPath [ "/usr" "/usr/local" ]
172 => "/usr/lib:/usr/local/lib"
173 pkgs = import <nixpkgs> { }
174 makeLibraryPath [ pkgs.openssl pkgs.zlib ]
175 => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r/lib:/nix/store/wwh7mhwh269sfjkm6k5665b5kgp7jrk2-zlib-1.2.8/lib"
177 makeLibraryPath = makeSearchPathOutput "lib" "lib";
179 /* Construct a binary search path (such as $PATH) containing the
180 binaries for a set of packages.
183 makeBinPath ["/root" "/usr" "/usr/local"]
184 => "/root/bin:/usr/bin:/usr/local/bin"
186 makeBinPath = makeSearchPathOutput "bin" "bin";
188 /* Normalize path, removing extranous /s
190 Type: normalizePath :: string -> string
193 normalizePath "/a//b///c/"
196 normalizePath = s: (builtins.foldl' (x: y: if y == "/" && hasSuffix "/" x then x else x+y) "" (stringToCharacters s));
198 /* Depending on the boolean `cond', return either the given string
199 or the empty string. Useful to concatenate against a bigger string.
201 Type: optionalString :: bool -> string -> string
204 optionalString true "some-string"
206 optionalString false "some-string"
212 # String to return if condition is true
213 string: if cond then string else "";
215 /* Determine whether a string has given prefix.
217 Type: hasPrefix :: string -> string -> bool
220 hasPrefix "foo" "foobar"
222 hasPrefix "foo" "barfoo"
226 # Prefix to check for
229 str: substring 0 (stringLength pref) str == pref;
231 /* Determine whether a string has given suffix.
233 Type: hasSuffix :: string -> string -> bool
236 hasSuffix "foo" "foobar"
238 hasSuffix "foo" "barfoo"
242 # Suffix to check for
247 lenContent = stringLength content;
248 lenSuffix = stringLength suffix;
249 in lenContent >= lenSuffix &&
250 substring (lenContent - lenSuffix) lenContent content == suffix;
252 /* Determine whether a string contains the given infix
254 Type: hasInfix :: string -> string -> bool
263 hasInfix "foo" "abcd"
266 hasInfix = infix: content:
267 builtins.match ".*${escapeRegex infix}.*" "${content}" != null;
269 /* Convert a string to a list of characters (i.e. singleton strings).
270 This allows you to, e.g., map a function over each character. However,
271 note that this will likely be horribly inefficient; Nix is not a
272 general purpose programming language. Complex string manipulations
273 should, if appropriate, be done in a derivation.
274 Also note that Nix treats strings as a list of bytes and thus doesn't
277 Type: stringToCharacters :: string -> [string]
280 stringToCharacters ""
282 stringToCharacters "abc"
284 stringToCharacters "💩"
285 => [ "�" "�" "�" "�" ]
287 stringToCharacters = s:
288 map (p: substring p 1 s) (lib.range 0 (stringLength s - 1));
290 /* Manipulate a string character by character and replace them by
291 strings before concatenating the results.
293 Type: stringAsChars :: (string -> string) -> string -> string
296 stringAsChars (x: if x == "a" then "i" else x) "nax"
300 # Function to map over each individual character
304 map f (stringToCharacters s)
307 /* Convert char to ascii value, must be in printable range
309 Type: charToInt :: string -> int
319 table = import ./ascii-table.nix;
320 in c: builtins.getAttr c table;
322 /* Escape occurrence of the elements of `list` in `string` by
323 prefixing it with a backslash.
325 Type: escape :: [string] -> string -> string
328 escape ["(" ")"] "(foo)"
331 escape = list: replaceChars list (map (c: "\\${c}") list);
333 /* Escape occurence of the element of `list` in `string` by
334 converting to its ASCII value and prefixing it with \\x.
335 Only works for printable ascii characters.
337 Type: escapeC = [string] -> string -> string
340 escapeC [" "] "foo bar"
344 escapeC = list: replaceChars list (map (c: "\\x${ toLower (lib.toHexString (charToInt c))}") list);
346 /* Quote string to be used safely within the Bourne shell.
348 Type: escapeShellArg :: string -> string
351 escapeShellArg "esc'ape\nme"
352 => "'esc'\\''ape\nme'"
354 escapeShellArg = arg: "'${replaceStrings ["'"] ["'\\''"] (toString arg)}'";
356 /* Quote all arguments to be safely passed to the Bourne shell.
358 Type: escapeShellArgs :: [string] -> string
361 escapeShellArgs ["one" "two three" "four'five"]
362 => "'one' 'two three' 'four'\\''five'"
364 escapeShellArgs = concatMapStringsSep " " escapeShellArg;
366 /* Test whether the given name is a valid POSIX shell variable name.
371 isValidPosixName "foo_bar000"
373 isValidPosixName "0-bad.jpg"
376 isValidPosixName = name: match "[a-zA-Z_][a-zA-Z0-9_]*" name != null;
378 /* Translate a Nix value into a shell variable declaration, with proper escaping.
380 The value can be a string (mapped to a regular variable), a list of strings
381 (mapped to a Bash-style array) or an attribute set of strings (mapped to a
382 Bash-style associative array). Note that "string" includes string-coercible
383 values like paths or derivations.
385 Strings are translated into POSIX sh-compatible code; lists and attribute sets
386 assume a shell that understands Bash syntax (e.g. Bash or ZSH).
388 Type: string -> (string | listOf string | attrsOf string) -> string
392 ${toShellVar "foo" "some string"}
393 [[ "$foo" == "some string" ]]
396 toShellVar = name: value:
397 lib.throwIfNot (isValidPosixName name) "toShellVar: ${name} is not a valid shell variable name" (
398 if isAttrs value && ! isCoercibleToString value then
399 "declare -A ${name}=(${
400 concatStringsSep " " (lib.mapAttrsToList (n: v:
401 "[${escapeShellArg n}]=${escapeShellArg v}"
404 else if isList value then
405 "declare -a ${name}=(${escapeShellArgs value})"
407 "${name}=${escapeShellArg value}"
410 /* Translate an attribute set into corresponding shell variable declarations
413 Type: attrsOf (string | listOf string | attrsOf string) -> string
420 ${toShellVars { inherit foo bar; }}
421 [[ "$foo" == "$bar" ]]
424 toShellVars = vars: concatStringsSep "\n" (lib.mapAttrsToList toShellVar vars);
426 /* Turn a string into a Nix expression representing that string
428 Type: string -> string
431 escapeNixString "hello\${}\n"
432 => "\"hello\\\${}\\n\""
434 escapeNixString = s: escape ["$"] (toJSON s);
436 /* Turn a string into an exact regular expression
438 Type: string -> string
441 escapeRegex "[^a-z]*"
444 escapeRegex = escape (stringToCharacters "\\[{()^$?*+|.");
446 /* Quotes a string if it can't be used as an identifier directly.
448 Type: string -> string
451 escapeNixIdentifier "hello"
453 escapeNixIdentifier "0abc"
456 escapeNixIdentifier = s:
457 # Regex from https://github.com/NixOS/nix/blob/d048577909e383439c2549e849c5c2f2016c997e/src/libexpr/lexer.l#L91
458 if match "[a-zA-Z_][a-zA-Z0-9_'-]*" s != null
459 then s else escapeNixString s;
461 /* Escapes a string such that it is safe to include verbatim in an XML
464 Type: string -> string
467 escapeXML ''"test" 'test' < & >''
468 => ""test" 'test' < & >"
470 escapeXML = builtins.replaceStrings
471 ["\"" "'" "<" ">" "&"]
472 [""" "'" "<" ">" "&"];
474 # Obsolete - use replaceStrings instead.
475 replaceChars = builtins.replaceStrings or (
478 substList = lib.zipLists del new;
480 let found = lib.findFirst (sub: sub.fst == c) null substList; in
481 if found == null then
486 stringAsChars subst s);
488 # Case conversion utilities.
489 lowerChars = stringToCharacters "abcdefghijklmnopqrstuvwxyz";
490 upperChars = stringToCharacters "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
492 /* Converts an ASCII string to lower-case.
494 Type: toLower :: string -> string
500 toLower = replaceChars upperChars lowerChars;
502 /* Converts an ASCII string to upper-case.
504 Type: toUpper :: string -> string
510 toUpper = replaceChars lowerChars upperChars;
512 /* Appends string context from another string. This is an implementation
515 Strings in Nix carry an invisible `context` which is a list of strings
516 representing store paths. If the string is later used in a derivation
517 attribute, the derivation will properly populate the inputDrvs and
521 pkgs = import <nixpkgs> { };
522 addContextFrom pkgs.coreutils "bar"
525 addContextFrom = a: b: substring 0 0 a + b;
527 /* Cut a string with a separator and produces a list of strings which
528 were separated by this separator.
531 splitString "." "foo.bar.baz"
532 => [ "foo" "bar" "baz" ]
533 splitString "/" "/usr/local/bin"
534 => [ "" "usr" "local" "bin" ]
536 splitString = _sep: _s:
538 sep = builtins.unsafeDiscardStringContext _sep;
539 s = builtins.unsafeDiscardStringContext _s;
540 splits = builtins.filter builtins.isString (builtins.split (escapeRegex sep) s);
542 map (v: addContextFrom _sep (addContextFrom _s v)) splits;
544 /* Return a string without the specified prefix, if the prefix matches.
546 Type: string -> string -> string
549 removePrefix "foo." "foo.bar.baz"
551 removePrefix "xxx" "foo.bar.baz"
555 # Prefix to remove if it matches
560 preLen = stringLength prefix;
561 sLen = stringLength str;
563 if hasPrefix prefix str then
564 substring preLen (sLen - preLen) str
568 /* Return a string without the specified suffix, if the suffix matches.
570 Type: string -> string -> string
573 removeSuffix "front" "homefront"
575 removeSuffix "xxx" "homefront"
579 # Suffix to remove if it matches
584 sufLen = stringLength suffix;
585 sLen = stringLength str;
587 if sufLen <= sLen && suffix == substring (sLen - sufLen) sufLen str then
588 substring 0 (sLen - sufLen) str
592 /* Return true if string v1 denotes a version older than v2.
595 versionOlder "1.1" "1.2"
597 versionOlder "1.1" "1.1"
600 versionOlder = v1: v2: compareVersions v2 v1 == 1;
602 /* Return true if string v1 denotes a version equal to or newer than v2.
605 versionAtLeast "1.1" "1.0"
607 versionAtLeast "1.1" "1.1"
609 versionAtLeast "1.1" "1.2"
612 versionAtLeast = v1: v2: !versionOlder v1 v2;
614 /* This function takes an argument that's either a derivation or a
615 derivation's "name" attribute and extracts the name part from that
619 getName "youtube-dl-2016.01.01"
621 getName pkgs.youtube-dl
626 parse = drv: (parseDrvName drv).name;
629 else x.pname or (parse x.name);
631 /* This function takes an argument that's either a derivation or a
632 derivation's "name" attribute and extracts the version part from that
636 getVersion "youtube-dl-2016.01.01"
638 getVersion pkgs.youtube-dl
643 parse = drv: (parseDrvName drv).version;
646 else x.version or (parse x.name);
648 /* Extract name with version from URL. Ask for separator which is
649 supposed to start extension.
652 nameFromURL "https://nixos.org/releases/nix/nix-1.7/nix-1.7-x86_64-linux.tar.bz2" "-"
654 nameFromURL "https://nixos.org/releases/nix/nix-1.7/nix-1.7-x86_64-linux.tar.bz2" "_"
657 nameFromURL = url: sep:
659 components = splitString "/" url;
660 filename = lib.last components;
661 name = head (splitString sep filename);
662 in assert name != filename; name;
664 /* Create an --{enable,disable}-<feat> string that can be passed to
665 standard GNU Autoconf scripts.
668 enableFeature true "shared"
670 enableFeature false "shared"
671 => "--disable-shared"
673 enableFeature = enable: feat:
674 assert isString feat; # e.g. passing openssl instead of "openssl"
675 "--${if enable then "enable" else "disable"}-${feat}";
677 /* Create an --{enable-<feat>=<value>,disable-<feat>} string that can be passed to
678 standard GNU Autoconf scripts.
681 enableFeatureAs true "shared" "foo"
682 => "--enable-shared=foo"
683 enableFeatureAs false "shared" (throw "ignored")
684 => "--disable-shared"
686 enableFeatureAs = enable: feat: value: enableFeature enable feat + optionalString enable "=${value}";
688 /* Create an --{with,without}-<feat> string that can be passed to
689 standard GNU Autoconf scripts.
692 withFeature true "shared"
694 withFeature false "shared"
695 => "--without-shared"
697 withFeature = with_: feat:
698 assert isString feat; # e.g. passing openssl instead of "openssl"
699 "--${if with_ then "with" else "without"}-${feat}";
701 /* Create an --{with-<feat>=<value>,without-<feat>} string that can be passed to
702 standard GNU Autoconf scripts.
705 withFeatureAs true "shared" "foo"
706 => "--with-shared=foo"
707 withFeatureAs false "shared" (throw "ignored")
708 => "--without-shared"
710 withFeatureAs = with_: feat: value: withFeature with_ feat + optionalString with_ "=${value}";
712 /* Create a fixed width string with additional prefix to match
715 This function will fail if the input string is longer than the
718 Type: fixedWidthString :: int -> string -> string -> string
721 fixedWidthString 5 "0" (toString 15)
724 fixedWidthString = width: filler: str:
726 strw = lib.stringLength str;
727 reqWidth = width - (lib.stringLength filler);
729 assert lib.assertMsg (strw <= width)
730 "fixedWidthString: requested string length (${
731 toString width}) must not be shorter than actual length (${
733 if strw == width then str else filler + fixedWidthString reqWidth filler str;
735 /* Format a number adding leading zeroes up to fixed width.
738 fixedWidthNumber 5 15
741 fixedWidthNumber = width: n: fixedWidthString width "0" (toString n);
743 /* Convert a float to a string, but emit a warning when precision is lost
744 during the conversion
747 floatToString 0.000001
749 floatToString 0.0000001
750 => trace: warning: Imprecise conversion from float to string 0.000000
753 floatToString = float: let
754 result = toString float;
755 precise = float == fromJSON result;
756 in lib.warnIf (!precise) "Imprecise conversion from float to string ${result}"
759 /* Check whether a value can be coerced to a string */
760 isCoercibleToString = x:
761 elem (typeOf x) [ "path" "string" "null" "int" "float" "bool" ] ||
762 (isList x && lib.all isCoercibleToString x) ||
766 /* Check whether a value is a store path.
769 isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11/bin/python"
771 isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11"
773 isStorePath pkgs.python
775 isStorePath [] || isStorePath 42 || isStorePath {} || …
779 if !(isList x) && isCoercibleToString x then
780 let str = toString x; in
781 substring 0 1 str == "/"
782 && dirOf str == storeDir
786 /* Parse a string as an int. Does not support parsing of integers with preceding zero due to
787 ambiguity between zero-padded and octal numbers. See toIntBase10.
803 => error: Ambiguity in interpretation of 00024 between octal and zero padded integer.
806 => error: floating point JSON numbers are not supported
810 # RegEx: Match any leading whitespace, then any digits, and finally match any trailing
812 strippedInput = match "[[:space:]]*([[:digit:]]+)[[:space:]]*" str;
814 # RegEx: Match a leading '0' then one or more digits.
815 isLeadingZero = match "0[[:digit:]]+" (head strippedInput) == [];
817 # Attempt to parse input
818 parsedInput = fromJSON (head strippedInput);
820 generalError = "toInt: Could not convert ${escapeNixString str} to int.";
822 octalAmbigError = "toInt: Ambiguity in interpretation of ${escapeNixString str}"
823 + " between octal and zero padded integer.";
826 # Error on presence of non digit characters.
827 if strippedInput == null
828 then throw generalError
829 # Error on presence of leading zero/octal ambiguity.
830 else if isLeadingZero
831 then throw octalAmbigError
832 # Error if parse function fails.
833 else if !isInt parsedInput
834 then throw generalError
839 /* Parse a string as a base 10 int. This supports parsing of zero-padded integers.
857 => error: floating point JSON numbers are not supported
861 # RegEx: Match any leading whitespace, then match any zero padding, capture any remaining
862 # digits after that, and finally match any trailing whitespace.
863 strippedInput = match "[[:space:]]*0*([[:digit:]]+)[[:space:]]*" str;
865 # RegEx: Match at least one '0'.
866 isZero = match "0+" (head strippedInput) == [];
868 # Attempt to parse input
869 parsedInput = fromJSON (head strippedInput);
871 generalError = "toIntBase10: Could not convert ${escapeNixString str} to int.";
874 # Error on presence of non digit characters.
875 if strippedInput == null
876 then throw generalError
877 # In the special case zero-padded zero (00000), return early.
880 # Error if parse function fails.
881 else if !isInt parsedInput
882 then throw generalError
886 /* Read a list of paths from `file`, relative to the `rootPath`.
887 Lines beginning with `#` are treated as comments and ignored.
888 Whitespace is significant.
890 NOTE: This function is not performant and should be avoided.
893 readPathsFromFile /prefix
894 ./pkgs/development/libraries/qt-5/5.4/qtbase/series
895 => [ "/prefix/dlopen-resolv.patch" "/prefix/tzdir.patch"
896 "/prefix/dlopen-libXcursor.patch" "/prefix/dlopen-openssl.patch"
897 "/prefix/dlopen-dbus.patch" "/prefix/xdg-config-dirs.patch"
898 "/prefix/nix-profiles-library-paths.patch"
899 "/prefix/compose-search-path.patch" ]
901 readPathsFromFile = lib.warn "lib.readPathsFromFile is deprecated, use a list instead"
904 lines = lib.splitString "\n" (readFile file);
905 removeComments = lib.filter (line: line != "" && !(lib.hasPrefix "#" line));
906 relativePaths = removeComments lines;
907 absolutePaths = map (path: rootPath + "/${path}") relativePaths;
911 /* Read the contents of a file removing the trailing \n
913 Type: fileContents :: path -> string
916 $ echo "1.0" > ./version
918 fileContents ./version
921 fileContents = file: removeSuffix "\n" (readFile file);
924 /* Creates a valid derivation name from a potentially invalid one.
926 Type: sanitizeDerivationName :: String -> String
929 sanitizeDerivationName "../hello.bar # foo"
931 sanitizeDerivationName ""
933 sanitizeDerivationName pkgs.hello
934 => "-nix-store-2g75chlbpxlrqn15zlby2dfh8hr9qwbk-hello-2.10"
936 sanitizeDerivationName =
937 let okRegex = match "[[:alnum:]+_?=-][[:alnum:]+._?=-]*";
940 # First detect the common case of already valid strings, to speed those up
941 if stringLength string <= 207 && okRegex string != null
942 then unsafeDiscardStringContext string
943 else lib.pipe string [
944 # Get rid of string context. This is safe under the assumption that the
945 # resulting string is only used as a derivation name
946 unsafeDiscardStringContext
947 # Strip all leading "."
948 (x: elemAt (match "\\.*(.*)" x) 0)
949 # Split out all invalid characters
950 # https://github.com/NixOS/nix/blob/2.3.2/src/libstore/store-api.cc#L85-L112
951 # https://github.com/NixOS/nix/blob/2242be83c61788b9c0736a92bb0b5c7bbfc40803/nix-rust/src/store/path.rs#L100-L125
952 (split "[^[:alnum:]+._?=-]+")
953 # Replace invalid character ranges with a "-"
954 (concatMapStrings (s: if lib.isList s then "-" else s))
955 # Limit to 211 characters (minus 4 chars for ".drv")
956 (x: substring (lib.max (stringLength x - 207) 0) (-1) x)
957 # If the result is empty, replace it with "unknown"
958 (x: if stringLength x == 0 then "unknown" else x)
961 /* Computes the Levenshtein distance between two strings.
962 Complexity O(n*m) where n and m are the lengths of the strings.
963 Algorithm adjusted from https://stackoverflow.com/a/9750974/6605742
965 Type: levenshtein :: string -> string -> int
968 levenshtein "foo" "foo"
970 levenshtein "book" "hook"
972 levenshtein "hello" "Heyo"
975 levenshtein = a: b: let
976 # Two dimensional array with dimensions (stringLength a + 1, stringLength b + 1)
977 arr = lib.genList (i:
980 ) (stringLength b + 1)
981 ) (stringLength a + 1);
982 d = x: y: lib.elemAt (lib.elemAt arr x) y;
984 let c = if substring (i - 1) 1 a == substring (j - 1) 1 b
988 else if i == 0 then j
990 ( lib.min (d (i - 1) j + 1) (d i (j - 1) + 1))
991 ( d (i - 1) (j - 1) + c );
992 in d (stringLength a) (stringLength b);
994 /* Returns the length of the prefix common to both strings.
996 commonPrefixLength = a: b:
998 m = lib.min (stringLength a) (stringLength b);
999 go = i: if i >= m then m else if substring i 1 a == substring i 1 b then go (i + 1) else i;
1002 /* Returns the length of the suffix common to both strings.
1004 commonSuffixLength = a: b:
1006 m = lib.min (stringLength a) (stringLength b);
1007 go = i: if i >= m then m else if substring (stringLength a - i - 1) 1 a == substring (stringLength b - i - 1) 1 b then go (i + 1) else i;
1010 /* Returns whether the levenshtein distance between two strings is at most some value
1011 Complexity is O(min(n,m)) for k <= 2 and O(n*m) otherwise
1013 Type: levenshteinAtMost :: int -> string -> string -> bool
1016 levenshteinAtMost 0 "foo" "foo"
1018 levenshteinAtMost 1 "foo" "boa"
1020 levenshteinAtMost 2 "foo" "boa"
1022 levenshteinAtMost 2 "This is a sentence" "this is a sentense."
1024 levenshteinAtMost 3 "This is a sentence" "this is a sentense."
1028 levenshteinAtMost = let
1029 infixDifferAtMost1 = x: y: stringLength x <= 1 && stringLength y <= 1;
1031 # This function takes two strings stripped by their common pre and suffix,
1032 # and returns whether they differ by at most two by Levenshtein distance.
1033 # Because of this stripping, if they do indeed differ by at most two edits,
1034 # we know that those edits were (if at all) done at the start or the end,
1035 # while the middle has to have stayed the same. This fact is used in the
1037 infixDifferAtMost2 = x: y:
1039 xlen = stringLength x;
1040 ylen = stringLength y;
1041 # This function is only called with |x| >= |y| and |x| - |y| <= 2, so
1042 # diff is one of 0, 1 or 2
1045 # Infix of x and y, stripped by the left and right most character
1046 xinfix = substring 1 (xlen - 2) x;
1047 yinfix = substring 1 (ylen - 2) y;
1049 # x and y but a character deleted at the left or right
1050 xdelr = substring 0 (xlen - 1) x;
1051 xdell = substring 1 (xlen - 1) x;
1052 ydelr = substring 0 (ylen - 1) y;
1053 ydell = substring 1 (ylen - 1) y;
1055 # A length difference of 2 can only be gotten with 2 delete edits,
1056 # which have to have happened at the start and end of x
1057 # Example: "abcdef" -> "bcde"
1058 if diff == 2 then xinfix == y
1059 # A length difference of 1 can only be gotten with a deletion on the
1060 # right and a replacement on the left or vice versa.
1061 # Example: "abcdef" -> "bcdez" or "zbcde"
1062 else if diff == 1 then xinfix == ydelr || xinfix == ydell
1063 # No length difference can either happen through replacements on both
1064 # sides, or a deletion on the left and an insertion on the right or
1066 # Example: "abcdef" -> "zbcdez" or "bcdefz" or "zabcde"
1067 else xinfix == yinfix || xdelr == ydell || xdell == ydelr;
1069 in k: if k <= 0 then a: b: a == b else
1072 alen = stringLength a;
1073 blen = stringLength b;
1074 prelen = commonPrefixLength a b;
1075 suflen = commonSuffixLength a b;
1076 presuflen = prelen + suflen;
1077 ainfix = substring prelen (alen - presuflen) a;
1078 binfix = substring prelen (blen - presuflen) b;
1080 # Make a be the bigger string
1081 if alen < blen then f b a
1082 # If a has over k more characters than b, even with k deletes on a, b can't be reached
1083 else if alen - blen > k then false
1084 else if k == 1 then infixDifferAtMost1 ainfix binfix
1085 else if k == 2 then infixDifferAtMost2 ainfix binfix
1086 else levenshtein ainfix binfix <= k;