Merge: Faster hex parsing
[nit.git] / lib / core / text / abstract_text.nit
index baf8ae7..fc8ec5b 100644 (file)
@@ -68,7 +68,7 @@ abstract class Text
        fun substring(from: Int, count: Int): SELFTYPE is abstract
 
        # Iterates on the substrings of self if any
-       fun substrings: Iterator[FlatText] is abstract
+       private fun substrings: Iterator[FlatText] is abstract
 
        # Is the current Text empty (== "")
        #
@@ -248,7 +248,17 @@ abstract class Text
        # If `self` contains only digits and alpha <= 'f', return the corresponding integer.
        #
        #     assert "ff".to_hex == 255
-       fun to_hex: Int do return a_to(16)
+       fun to_hex(pos, ln: nullable Int): Int do
+               var res = 0
+               if pos == null then pos = 0
+               if ln == null then ln = length - pos
+               var max = pos + ln
+               for i in [pos .. max[ do
+                       res <<= 4
+                       res += self[i].from_hex
+               end
+               return res
+       end
 
        # If `self` contains only digits <= '7', return the corresponding integer.
        #
@@ -718,6 +728,49 @@ abstract class Text
                return res.to_s
        end
 
+       # Returns `self` with all characters escaped with their UTF-16 representation
+       #
+       #     assert "Aรจใ‚๐“".escape_to_utf16 == "\\u0041\\u00e8\\u3042\\ud800\\udfd3"
+       fun escape_to_utf16: String do
+               var buf = new Buffer
+               for i in chars do buf.append i.escape_to_utf16
+               return buf.to_s
+       end
+
+       # Returns the Unicode char escaped by `self`
+       #
+       #     assert "\\u0041".from_utf16_escape == 'A'
+       #     assert "\\ud800\\udfd3".from_utf16_escape == '๐“'
+       #     assert "\\u00e8".from_utf16_escape == 'รจ'
+       #     assert "\\u3042".from_utf16_escape == 'ใ‚'
+       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
+               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
        #
        #     assert "aBc09-._~".to_percent_encoding == "aBc09-._~"
@@ -942,6 +995,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
@@ -966,7 +1078,7 @@ abstract class Text
 end
 
 # All kinds of array-based text representations.
-private abstract class FlatText
+abstract class FlatText
        super Text
 
        # Underlying C-String (`char*`)
@@ -1568,6 +1680,12 @@ redef class Char
        #     assert 'ใพ'.bytes == [0xE3u8, 0x81u8, 0xBEu8]
        fun bytes: SequenceRead[Byte] do return to_s.bytes
 
+       # Is `self` an UTF-16 surrogate pair ?
+       fun is_surrogate: Bool do
+               var cp = code_point
+               return cp >= 0xD800 and cp <= 0xDFFF
+       end
+
        # Length of `self` in a UTF-8 String
        private fun u8char_len: Int do
                var c = self.code_point
@@ -1587,6 +1705,46 @@ redef class Char
                return ns.to_s_with_length(ln)
        end
 
+       # Returns `self` escaped to UTF-16
+       #
+       # i.e. Represents `self`.`code_point` using UTF-16 codets escaped
+       # with a `\u`
+       #
+       #     assert 'A'.escape_to_utf16 == "\\u0041"
+       #     assert 'รจ'.escape_to_utf16 == "\\u00e8"
+       #     assert 'ใ‚'.escape_to_utf16 == "\\u3042"
+       #     assert '๐“'.escape_to_utf16 == "\\ud800\\udfd3"
+       fun escape_to_utf16: String do
+               var cp = code_point
+               var buf: Buffer
+               if cp < 0xD800 or (cp >= 0xE000 and cp <= 0xFFFF) then
+                       buf = new Buffer.with_cap(6)
+                       buf.append("\\u0000")
+                       var hx = cp.to_hex
+                       var outid = 5
+                       for i in hx.chars.reverse_iterator do
+                               buf[outid] = i
+                               outid -= 1
+                       end
+               else
+                       buf = new Buffer.with_cap(12)
+                       buf.append("\\u0000\\u0000")
+                       var lo = (((cp - 0x10000) & 0x3FF) + 0xDC00).to_hex
+                       var hi = ((((cp - 0x10000) & 0xFFC00) >> 10) + 0xD800).to_hex
+                       var out = 2
+                       for i in hi do
+                               buf[out] = i
+                               out += 1
+                       end
+                       out = 8
+                       for i in lo do
+                               buf[out] = i
+                               out += 1
+                       end
+               end
+               return buf.to_s
+       end
+
        private fun u8char_tos(r: NativeString, len: Int) `{
                r[len] = '\0';
                switch(len){
@@ -1637,6 +1795,16 @@ redef class Char
                return (self >= 'a' and self <= 'z') or (self >= 'A' and self <= 'Z')
        end
 
+       # Is `self` an hexadecimal digit ?
+       #
+       #     assert 'A'.is_hexdigit
+       #     assert not 'G'.is_hexdigit
+       #     assert 'a'.is_hexdigit
+       #     assert not 'g'.is_hexdigit
+       #     assert '5'.is_hexdigit
+       fun is_hexdigit: Bool do return (self >= '0' and self <= '9') or (self >= 'A' and self <= 'F') or
+                                       (self >= 'a' and self <= 'f')
+
        # Returns true if the char is an alpha or a numeric digit
        #
        #     assert 'a'.is_alphanumeric
@@ -1650,6 +1818,19 @@ redef class Char
        do
                return self.is_numeric or self.is_alpha
        end
+
+       # Returns `self` to its int value
+       #
+       # REQUIRE: `is_hexdigit`
+       fun from_hex: Int do
+               if self >= '0' and self <= '9' then return code_point - 0x30
+               if self >= 'A' and self <= 'F' then return code_point - 0x37
+               if self >= 'a' and self <= 'f' then return code_point - 0x57
+               # Happens if self is not a hexdigit
+               assert self.is_hexdigit
+               # To make flow analysis happy
+               abort
+       end
 end
 
 redef class Collection[E]