lib/text: `Text::format` harder to break and using `%%` as escape
[nit.git] / lib / core / text / abstract_text.nit
index 1de1aa1..0d2848c 100644 (file)
@@ -305,26 +305,32 @@ abstract class Text
                end
        end
 
-       # Returns `true` if the string contains only Numeric values (and one "," or one "." character)
+       # Is this string in a valid numeric format compatible with `to_f`?
        #
        #     assert "123".is_numeric  == true
        #     assert "1.2".is_numeric  == true
-       #     assert "1,2".is_numeric  == true
+       #     assert "-1.2".is_numeric == true
+       #     assert "-1.23e-2".is_numeric == true
        #     assert "1..2".is_numeric == false
+       #     assert "".is_numeric     == false
        fun is_numeric: Bool
        do
-               var has_point_or_comma = false
+               var has_point = false
+               var e_index = -1
                for i in [0..length[ do
                        var c = chars[i]
                        if not c.is_numeric then
-                               if (c == '.' or c == ',') and not has_point_or_comma then
-                                       has_point_or_comma = true
+                               if c == '.' and not has_point then
+                                       has_point = true
+                               else if c == 'e' and e_index == -1 and i > 0 and i < length - 1 and chars[i-1] != '-' then
+                                       e_index = i
+                               else if c == '-' and i == e_index + 1 and i < length - 1 then
                                else
                                        return false
                                end
                        end
                end
-               return true
+               return not is_empty
        end
 
        # Returns `true` if the string contains only Hex chars
@@ -743,21 +749,32 @@ abstract class Text
        #     assert "\\ud800\\udfd3".from_utf16_escape == '๐“'
        #     assert "\\u00e8".from_utf16_escape == 'รจ'
        #     assert "\\u3042".from_utf16_escape == 'ใ‚'
-       fun from_utf16_escape: Char do
-               var ln = length
-               if ln != 6 and ln != 12 then return 0xFFFD.code_point
-               var cphi = substring(2, 4).to_hex
-               if cphi < 0xD800 then return cphi.code_point
-               if cphi > 0xDFFF then return cphi.code_point
-               if cphi > 0xDBFF then return 0xFFFD.code_point
-               var cp = 0
-               cp += (cphi - 0xD800) << 10
-               var cplo = substring(8, 4).to_hex
+       fun from_utf16_escape(pos, ln: nullable Int): Char do
+               if pos == null then pos = 0
+               if ln == null then ln = length - pos
+               if ln < 6 then return 0xFFFD.code_point
+               var cp = from_utf16_digit(pos + 2)
+               if cp < 0xD800 then return cp.code_point
+               if cp > 0xDFFF then return cp.code_point
+               if cp > 0xDBFF then return 0xFFFD.code_point
+               if ln == 6 then return 0xFFFD.code_point
+               if ln < 12 then return 0xFFFD.code_point
+               cp <<= 16
+               cp += from_utf16_digit(pos + 8)
+               var cplo = cp & 0xFFFF
                if cplo < 0xDC00 then return 0xFFFD.code_point
                if cplo > 0xDFFF then return 0xFFFD.code_point
-               cp += cplo - 0xDC00
-               cp += 0x10000
-               return cp.code_point
+               return cp.from_utf16_surr.code_point
+       end
+
+       # Returns a UTF-16 escape value
+       #
+       #     var s = "\\ud800\\udfd3"
+       #     assert s.from_utf16_digit(2) == 0xD800
+       #     assert s.from_utf16_digit(8) == 0xDFD3
+       fun from_utf16_digit(pos: nullable Int): Int do
+               if pos == null then pos = 0
+               return to_hex(pos, 4)
        end
 
        # Encode `self` to percent (or URL) encoding
@@ -841,7 +858,7 @@ abstract class Text
                        l += 1
                end
 
-               return buf.to_s_with_length(l)
+               return buf.to_s_unsafe(l)
        end
 
        # Escape the characters `<`, `>`, `&`, `"`, `'` and `/` as HTML/XML entity references.
@@ -947,36 +964,46 @@ abstract class Text
                return hash_cache.as(not null)
        end
 
-       # Gives the formatted string back as a Nit string with `args` in place
+       # Format `self` by replacing each `%n` with the `n`th item of `args`
        #
-       #     assert "This %1 is a %2.".format("String", "formatted String") == "This String is a formatted String."
-       #     assert "\\%1 This string".format("String") == "\\%1 This string"
+       # The character `%` followed by something other than a number are left as is.
+       # To represent a `%` followed by a number, double the `%`, as in `%%7`.
+       #
+       #     assert "This %0 is a %1.".format("String", "formatted String") == "This String is a formatted String."
+       #     assert "Do not escape % nor %%1".format("unused") == "Do not escape % nor %1"
        fun format(args: Object...): String do
                var s = new Array[Text]
                var curr_st = 0
                var i = 0
                while i < length do
-                       # Skip escaped characters
-                       if self[i] == '\\' then
-                               i += 1
-                       # In case of format
-                       else if self[i] == '%' then
+                       if self[i] == '%' then
                                var fmt_st = i
                                i += 1
                                var ciph_st = i
                                while i < length and self[i].is_numeric do
                                        i += 1
                                end
-                               i -= 1
-                               var fmt_end = i
-                               var ciph_len = fmt_end - ciph_st + 1
 
-                               var arg_index = substring(ciph_st, ciph_len).to_i - 1
+                               var ciph_len = i - ciph_st
+                               if ciph_len == 0 then
+                                       # What follows '%' is not a number.
+                                       s.push substring(curr_st, i - curr_st)
+                                       if i < length and self[i] == '%' then
+                                               # Skip the next `%`
+                                               i += 1
+                                       end
+                                       curr_st = i
+                                       continue
+                               end
+
+                               var arg_index = substring(ciph_st, ciph_len).to_i
                                if arg_index >= args.length then continue
 
                                s.push substring(curr_st, fmt_st - curr_st)
                                s.push args[arg_index].to_s
-                               curr_st = i + 1
+
+                               curr_st = i
+                               i -= 1
                        end
                        i += 1
                end
@@ -984,6 +1011,65 @@ abstract class Text
                return s.plain_to_s
        end
 
+       # Return the Levenshtein distance between two strings
+       #
+       # ~~~
+       # assert "abcd".levenshtein_distance("abcd") == 0
+       # assert "".levenshtein_distance("abcd")     == 4
+       # assert "abcd".levenshtein_distance("")     == 4
+       # assert "abcd".levenshtein_distance("xyz")  == 4
+       # assert "abcd".levenshtein_distance("xbdy") == 3
+       # ~~~
+       fun levenshtein_distance(other: String): Int
+       do
+               var slen = self.length
+               var olen = other.length
+
+               # fast cases
+               if slen == 0 then return olen
+               if olen == 0 then return slen
+               if self == other then return 0
+
+               # previous row of distances
+               var v0 = new Array[Int].with_capacity(olen+1)
+
+               # current row of distances
+               var v1 = new Array[Int].with_capacity(olen+1)
+
+               for j in [0..olen] do
+                       # prefix insert cost
+                       v0[j] = j
+               end
+
+               for i in [0..slen[ do
+
+                       # prefix delete cost
+                       v1[0] = i + 1
+
+                       for j in [0..olen[ do
+                               # delete cost
+                               var cost1 = v1[j] + 1
+                               # insert cost
+                               var cost2 = v0[j + 1] + 1
+                               # same char cost (+0)
+                               var cost3 = v0[j]
+                               # change cost
+                               if self[i] != other[j] then cost3 += 1
+                               # keep the min
+                               v1[j+1] = cost1.min(cost2).min(cost3)
+                       end
+
+                       # Switch columns:
+                       # * v1 become v0 in the next iteration
+                       # * old v0 is reused as the new v1
+                       var tmp = v1
+                       v1 = v0
+                       v0 = tmp
+               end
+
+               return v0[olen]
+       end
+
        # Copies `n` bytes from `self` at `src_offset` into `dest` starting at `dest_offset`
        #
        # Basically a high-level synonym of NativeString::copy_to
@@ -992,7 +1078,7 @@ abstract class Text
        #
        #       var ns = new NativeString(8)
        #       "Text is String".copy_to_native(ns, 8, 2, 0)
-       #       assert ns.to_s_with_length(8) == "xt is St"
+       #       assert ns.to_s_unsafe(8) == "xt is St"
        #
        fun copy_to_native(dest: NativeString, n, src_offset, dest_offset: Int) do
                var mypos = src_offset
@@ -1471,14 +1557,14 @@ redef class Byte
                var ns = new NativeString(nslen + 1)
                ns[nslen] = 0u8
                native_byte_to_s(ns, nslen + 1)
-               return ns.to_s_with_length(nslen)
+               return ns.to_s_unsafe(nslen)
        end
 end
 
 redef class Int
 
        # Wrapper of strerror C function
-       private fun strerror_ext: NativeString `{ return strerror(self); `}
+       private fun strerror_ext: NativeString `{ return strerror((int)self); `}
 
        # Returns a string describing error number
        fun strerror: String do return strerror_ext.to_s
@@ -1632,7 +1718,7 @@ redef class Char
                var ln = u8char_len
                var ns = new NativeString(ln + 1)
                u8char_tos(ns, ln)
-               return ns.to_s_with_length(ln)
+               return ns.to_s_unsafe(ln)
        end
 
        # Returns `self` escaped to UTF-16
@@ -1931,6 +2017,12 @@ redef class NativeString
        # Returns `self` as a String of `length`.
        fun to_s_with_length(length: Int): String is abstract
 
+       # Returns a new instance of `String` with self as `_items`
+       #
+       # /!\: Does not clean the items for compliance with UTF-8,
+       # Use only if you know what you are doing
+       fun to_s_unsafe(len: nullable Int): String is abstract
+
        # Returns `self` as a String with `bytelen` and `length` set
        #
        # SEE: `abstract_text::Text` for more infos on the difference