X-Git-Url: http://nitlanguage.org diff --git a/lib/core/text/abstract_text.nit b/lib/core/text/abstract_text.nit index b1e6a04..1b4b383 100644 --- a/lib/core/text/abstract_text.nit +++ b/lib/core/text/abstract_text.nit @@ -25,6 +25,7 @@ in "C" `{ # High-level abstraction for all text representations abstract class Text super Comparable + super Cloneable redef type OTHER: Text @@ -33,34 +34,44 @@ abstract class Text # Gets a view on the chars of the Text object # - # assert "hello".chars.to_a == ['h', 'e', 'l', 'l', 'o'] + # ~~~ + # assert "hello".chars.to_a == ['h', 'e', 'l', 'l', 'o'] + # ~~~ fun chars: SequenceRead[Char] is abstract # Gets a view on the bytes of the Text object # - # assert "hello".bytes.to_a == [104u8, 101u8, 108u8, 108u8, 111u8] - fun bytes: SequenceRead[Byte] is abstract + # ~~~ + # assert "hello".bytes.to_a == [104, 101, 108, 108, 111] + # ~~~ + fun bytes: SequenceRead[Int] is abstract # Number of characters contained in self. # - # assert "12345".length == 5 - # assert "".length == 0 - # assert "あいうえお".length == 5 + # ~~~ + # assert "12345".length == 5 + # assert "".length == 0 + # assert "あいうえお".length == 5 + # ~~~ fun length: Int is abstract # Number of bytes in `self` # - # assert "12345".bytelen == 5 - # assert "あいうえお".bytelen == 15 - fun bytelen: Int is abstract + # ~~~ + # assert "12345".byte_length == 5 + # assert "あいうえお".byte_length == 15 + # ~~~ + fun byte_length: Int is abstract # Create a substring. # - # assert "abcd".substring(1, 2) == "bc" - # assert "abcd".substring(-1, 2) == "a" - # assert "abcd".substring(1, 0) == "" - # assert "abcd".substring(2, 5) == "cd" - # assert "あいうえお".substring(1,3) == "いうえ" + # ~~~ + # assert "abcd".substring(1, 2) == "bc" + # assert "abcd".substring(-1, 2) == "a" + # assert "abcd".substring(1, 0) == "" + # assert "abcd".substring(2, 5) == "cd" + # assert "あいうえお".substring(1,3) == "いうえ" + # ~~~ # # A `from` index < 0 will be replaced by 0. # Unless a `count` value is > 0 at the same time. @@ -68,12 +79,14 @@ 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 (== "") # - # assert "".is_empty - # assert not "foo".is_empty + # ~~~ + # assert "".is_empty + # assert not "foo".is_empty + # ~~~ fun is_empty: Bool do return self.length == 0 # Returns an empty Text of the right type @@ -82,38 +95,33 @@ abstract class Text # implementation of an empty string. protected fun empty: SELFTYPE is abstract + # Returns a copy of `self` as a Buffer + fun to_buffer: Buffer is abstract + # Gets the first char of the Text - # - # DEPRECATED : Use self.chars.first instead fun first: Char do return self.chars[0] # Access a character at `index` in the string. # - # assert "abcd"[2] == 'c' - # - # DEPRECATED : Use self.chars.[] instead + # ~~~ + # assert "abcd"[2] == 'c' + # ~~~ fun [](index: Int): Char do return self.chars[index] # Gets the index of the first occurence of 'c' # # Returns -1 if not found - # - # DEPRECATED : Use self.chars.index_of instead fun index_of(c: Char): Int do return index_of_from(c, 0) end # Gets the last char of self - # - # DEPRECATED : Use self.chars.last instead fun last: Char do return self.chars[length-1] # Gets the index of the first occurence of ´c´ starting from ´pos´ # # Returns -1 if not found - # - # DEPRECATED : Use self.chars.index_of_from instead fun index_of_from(c: Char, pos: Int): Int do var iter = self.chars.iterator_from(pos) @@ -127,38 +135,34 @@ abstract class Text # Gets the last index of char ´c´ # # Returns -1 if not found - # - # DEPRECATED : Use self.chars.last_index_of instead fun last_index_of(c: Char): Int do return last_index_of_from(c, length - 1) end # Return a null terminated char * - fun to_cstring: NativeString is abstract + fun to_cstring: CString is abstract # The index of the last occurrence of an element starting from pos (in reverse order). # - # var s = "/etc/bin/test/test.nit" - # assert s.last_index_of_from('/', s.length-1) == 13 - # assert s.last_index_of_from('/', 12) == 8 + # ~~~ + # var s = "/etc/bin/test/test.nit" + # assert s.last_index_of_from('/', s.length-1) == 13 + # assert s.last_index_of_from('/', 12) == 8 + # ~~~ # # Returns -1 if not found + fun last_index_of_from(item: Char, pos: Int): Int do return chars.last_index_of_from(item, pos) + + # Concatenates `o` to `self` # - # DEPRECATED : Use self.chars.last_index_of_from instead - fun last_index_of_from(item: Char, pos: Int): Int - do - var iter = self.chars.reverse_iterator_from(pos) - while iter.is_ok do - if iter.item == item then return iter.index - iter.next - end - return -1 - end + # ~~~ + # assert "hello" + "world" == "helloworld" + # assert "" + "hello" + "" == "hello" + # ~~~ + fun +(o: Text): SELFTYPE is abstract # Gets an iterator on the chars of self - # - # DEPRECATED : Use self.chars.iterator instead fun iterator: Iterator[Char] do return self.chars.iterator @@ -166,15 +170,15 @@ abstract class Text # Gets an Array containing the chars of self - # - # DEPRECATED : Use self.chars.to_a instead fun to_a: Array[Char] do return chars.to_a # Create a substring from `self` beginning at the `from` position # - # assert "abcd".substring_from(1) == "bcd" - # assert "abcd".substring_from(-1) == "abcd" - # assert "abcd".substring_from(2) == "cd" + # ~~~ + # assert "abcd".substring_from(1) == "bcd" + # assert "abcd".substring_from(-1) == "abcd" + # assert "abcd".substring_from(2) == "cd" + # ~~~ # # As with substring, a `from` index < 0 will be replaced by 0 fun substring_from(from: Int): SELFTYPE @@ -186,8 +190,10 @@ abstract class Text # Does self have a substring `str` starting from position `pos`? # - # assert "abcd".has_substring("bc",1) == true - # assert "abcd".has_substring("bc",2) == false + # ~~~ + # assert "abcd".has_substring("bc",1) == true + # assert "abcd".has_substring("bc",2) == false + # ~~~ # # Returns true iff all characters of `str` are presents # at the expected index in `self.` @@ -196,13 +202,17 @@ abstract class Text # # This means that all characters of `str` need to be inside `self`. # - # assert "abcd".has_substring("xab", -1) == false - # assert "abcd".has_substring("cdx", 2) == false + # ~~~ + # assert "abcd".has_substring("xab", -1) == false + # assert "abcd".has_substring("cdx", 2) == false + # ~~~ # # And that the empty string is always a valid substring. # - # assert "abcd".has_substring("", 2) == true - # assert "abcd".has_substring("", 200) == true + # ~~~ + # assert "abcd".has_substring("", 2) == true + # assert "abcd".has_substring("", 200) == true + # ~~~ fun has_substring(str: String, pos: Int): Bool do if str.is_empty then return true @@ -220,33 +230,42 @@ abstract class Text # Is this string prefixed by `prefix`? # - # assert "abcd".has_prefix("ab") == true - # assert "abcbc".has_prefix("bc") == false - # assert "ab".has_prefix("abcd") == false + # ~~~ + # assert "abcd".has_prefix("ab") == true + # assert "abcbc".has_prefix("bc") == false + # assert "ab".has_prefix("abcd") == false + # ~~~ fun has_prefix(prefix: String): Bool do return has_substring(prefix,0) # Is this string suffixed by `suffix`? # - # assert "abcd".has_suffix("abc") == false - # assert "abcd".has_suffix("bcd") == true + # ~~~ + # assert "abcd".has_suffix("abc") == false + # assert "abcd".has_suffix("bcd") == true + # ~~~ fun has_suffix(suffix: String): Bool do return has_substring(suffix, length - suffix.length) # Returns `self` as the corresponding integer # - # assert "123".to_i == 123 - # assert "-1".to_i == -1 - # assert "0x64".to_i == 100 - # assert "0b1100_0011".to_i== 195 - # assert "--12".to_i == 12 + # ~~~ + # assert "123".to_i == 123 + # assert "-1".to_i == -1 + # assert "0x64".to_i == 100 + # assert "0b1100_0011".to_i== 195 + # assert "--12".to_i == 12 + # assert "+45".to_i == 45 + # ~~~ # # REQUIRE: `self`.`is_int` fun to_i: Int is abstract # If `self` contains a float, return the corresponding float # - # assert "123".to_f == 123.0 - # assert "-1".to_f == -1.0 - # assert "-1.2e-3".to_f == -0.0012 + # ~~~ + # assert "123".to_f == 123.0 + # assert "-1".to_f == -1.0 + # assert "-1.2e-3".to_f == -0.0012 + # ~~~ fun to_f: Float do # Shortcut @@ -255,27 +274,47 @@ 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) + # ~~~ + # assert "ff".to_hex == 255 + # ~~~ + 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. # - # assert "714".to_oct == 460 + # ~~~ + # assert "714".to_oct == 460 + # ~~~ fun to_oct: Int do return a_to(8) # If `self` contains only '0' et '1', return the corresponding integer. # - # assert "101101".to_bin == 45 + # ~~~ + # assert "101101".to_bin == 45 + # ~~~ fun to_bin: Int do return a_to(2) # If `self` contains only digits '0' .. '9', return the corresponding integer. # - # assert "108".to_dec == 108 + # ~~~ + # assert "108".to_dec == 108 + # ~~~ fun to_dec: Int do return a_to(10) # If `self` contains only digits and letters, return the corresponding integer in a given base # - # assert "120".a_to(3) == 15 + # ~~~ + # assert "120".a_to(3) == 15 + # ~~~ fun a_to(base: Int) : Int do var i = 0 @@ -303,33 +342,43 @@ 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 == false + # ~~~ + # assert "123".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 # - # assert "048bf".is_hex == true - # assert "ABCDEF".is_hex == true - # assert "0G".is_hex == false + # ~~~ + # assert "048bf".is_hex == true + # assert "ABCDEF".is_hex == true + # assert "0G".is_hex == false + # ~~~ fun is_hex: Bool do for i in [0..length[ do @@ -343,8 +392,10 @@ abstract class Text # Returns `true` if the string contains only Binary digits # - # assert "1101100".is_bin == true - # assert "1101020".is_bin == false + # ~~~ + # assert "1101100".is_bin == true + # assert "1101020".is_bin == false + # ~~~ fun is_bin: Bool do for i in chars do if i != '0' and i != '1' then return false return true @@ -352,8 +403,10 @@ abstract class Text # Returns `true` if the string contains only Octal digits # - # assert "213453".is_oct == true - # assert "781".is_oct == false + # ~~~ + # assert "213453".is_oct == true + # assert "781".is_oct == false + # ~~~ fun is_oct: Bool do for i in chars do if i < '0' or i > '7' then return false return true @@ -361,8 +414,10 @@ abstract class Text # Returns `true` if the string contains only Decimal digits # - # assert "10839".is_dec == true - # assert "164F".is_dec == false + # ~~~ + # assert "10839".is_dec == true + # assert "164F".is_dec == false + # ~~~ fun is_dec: Bool do for i in chars do if i < '0' or i > '9' then return false return true @@ -370,10 +425,12 @@ abstract class Text # Are all letters in `self` upper-case ? # - # assert "HELLO WORLD".is_upper == true - # assert "%$&%!".is_upper == true - # assert "hello world".is_upper == false - # assert "Hello World".is_upper == false + # ~~~ + # assert "HELLO WORLD".is_upper == true + # assert "%$&%!".is_upper == true + # assert "hello world".is_upper == false + # assert "Hello World".is_upper == false + # ~~~ fun is_upper: Bool do for i in [0..length[ do @@ -385,9 +442,11 @@ abstract class Text # Are all letters in `self` lower-case ? # - # assert "hello world".is_lower == true - # assert "%$&%!".is_lower == true - # assert "Hello World".is_lower == false + # ~~~ + # assert "hello world".is_lower == true + # assert "%$&%!".is_lower == true + # assert "Hello World".is_lower == false + # ~~~ fun is_lower: Bool do for i in [0..length[ do @@ -399,7 +458,9 @@ abstract class Text # Removes the whitespaces at the beginning of self # - # assert " \n\thello \n\t".l_trim == "hello \n\t" + # ~~~ + # assert " \n\thello \n\t".l_trim == "hello \n\t" + # ~~~ # # `Char::is_whitespace` determines what is a whitespace. fun l_trim: SELFTYPE @@ -415,7 +476,9 @@ abstract class Text # Removes the whitespaces at the end of self # - # assert " \n\thello \n\t".r_trim == " \n\thello" + # ~~~ + # assert " \n\thello \n\t".r_trim == " \n\thello" + # ~~~ # # `Char::is_whitespace` determines what is a whitespace. fun r_trim: SELFTYPE @@ -431,17 +494,21 @@ abstract class Text # Trims trailing and preceding white spaces # - # assert " Hello World ! ".trim == "Hello World !" - # assert "\na\nb\tc\t".trim == "a\nb\tc" + # ~~~ + # assert " Hello World ! ".trim == "Hello World !" + # assert "\na\nb\tc\t".trim == "a\nb\tc" + # ~~~ # # `Char::is_whitespace` determines what is a whitespace. fun trim: SELFTYPE do return (self.l_trim).r_trim # Is the string non-empty but only made of whitespaces? # - # assert " \n\t ".is_whitespace == true - # assert " hello ".is_whitespace == false - # assert "".is_whitespace == false + # ~~~ + # assert " \n\t ".is_whitespace == true + # assert " hello ".is_whitespace == false + # assert "".is_whitespace == false + # ~~~ # # `Char::is_whitespace` determines what is a whitespace. fun is_whitespace: Bool @@ -455,19 +522,23 @@ abstract class Text # Returns `self` removed from its last line terminator (if any). # - # assert "Hello\n".chomp == "Hello" - # assert "Hello".chomp == "Hello" + # ~~~ + # assert "Hello\n".chomp == "Hello" + # assert "Hello".chomp == "Hello" # - # assert "\n".chomp == "" - # assert "".chomp == "" + # assert "\n".chomp == "" + # assert "".chomp == "" + # ~~~ # # Line terminators are `"\n"`, `"\r\n"` and `"\r"`. # A single line terminator, the last one, is removed. # - # assert "\r\n".chomp == "" - # assert "\r\n\n".chomp == "\r\n" - # assert "\r\n\r\n".chomp == "\r\n" - # assert "\r\n\r".chomp == "\r\n" + # ~~~ + # assert "\r\n".chomp == "" + # assert "\r\n\n".chomp == "\r\n" + # assert "\r\n\r\n".chomp == "\r\n" + # assert "\r\n\r".chomp == "\r\n" + # ~~~ # # Note: unlike with most IO methods like `Reader::read_line`, # a single `\r` is considered here to be a line terminator and will be removed. @@ -487,33 +558,41 @@ abstract class Text end end - # Justify a self in a space of `length` + # Justify `self` in a space of `length` # # `left` is the space ratio on the left side. # * 0.0 for left-justified (no space at the left) # * 1.0 for right-justified (all spaces at the left) # * 0.5 for centered (half the spaces at the left) # + # `char`, or `' '` by default, is repeated to pad the empty space. + # # Examples # - # assert "hello".justify(10, 0.0) == "hello " - # assert "hello".justify(10, 1.0) == " hello" - # assert "hello".justify(10, 0.5) == " hello " + # ~~~ + # assert "hello".justify(10, 0.0) == "hello " + # assert "hello".justify(10, 1.0) == " hello" + # assert "hello".justify(10, 0.5) == " hello " + # assert "hello".justify(10, 0.5, '.') == "..hello..." + # ~~~ # # If `length` is not enough, `self` is returned as is. # - # assert "hello".justify(2, 0.0) == "hello" + # ~~~ + # assert "hello".justify(2, 0.0) == "hello" + # ~~~ # # REQUIRE: `left >= 0.0 and left <= 1.0` # ENSURE: `self.length <= length implies result.length == length` # ENSURE: `self.length >= length implies result == self` - fun justify(length: Int, left: Float): String + fun justify(length: Int, left: Float, char: nullable Char): String do + var pad = (char or else ' ').to_s var diff = length - self.length if diff <= 0 then return to_s assert left >= 0.0 and left <= 1.0 var before = (diff.to_f * left).to_i - return " " * before + self + " " * (diff-before) + return pad * before + self + pad * (diff-before) end # Mangle a string to be a unique string only made of alphanumeric characters and underscores. @@ -526,36 +605,40 @@ abstract class Text # * Never ends with an underscore. # * Never contains two contiguous underscores. # - # assert "42_is/The answer!".to_cmangle == "_52d2_is_47dThe_32danswer_33d" - # assert "__".to_cmangle == "_95d_95d" - # assert "__d".to_cmangle == "_95d_d" - # assert "_d_".to_cmangle == "_d_95d" - # assert "_42".to_cmangle == "_95d42" - # assert "foo".to_cmangle == "foo" - # assert "".to_cmangle == "" + # Examples: + # + # ~~~ + # assert "42_is/The answer!".to_cmangle == "_52d2_is_47dThe_32danswer_33d" + # assert "__".to_cmangle == "_95d_95d" + # assert "__d".to_cmangle == "_95d_d" + # assert "_d_".to_cmangle == "_d_95d" + # assert "_42".to_cmangle == "_95d42" + # assert "foo".to_cmangle == "foo" + # assert "".to_cmangle == "" + # ~~~ fun to_cmangle: String do if is_empty then return "" var res = new Buffer var underscore = false var start = 0 - var c = chars[0] + var c = self[0] if c >= '0' and c <= '9' then res.add('_') - res.append(c.ascii.to_s) + res.append(c.code_point.to_s) res.add('d') start = 1 end for i in [start..length[ do - c = chars[i] + c = self[i] if (c >= 'a' and c <= 'z') or (c >='A' and c <= 'Z') then res.add(c) underscore = false continue end if underscore then - res.append('_'.ascii.to_s) + res.append('_'.code_point.to_s) res.add('d') end if c >= '0' and c <= '9' then @@ -566,28 +649,35 @@ abstract class Text underscore = true else res.add('_') - res.append(c.ascii.to_s) + res.append(c.code_point.to_s) res.add('d') underscore = false end end if underscore then - res.append('_'.ascii.to_s) + res.append('_'.code_point.to_s) res.add('d') end return res.to_s end - # Escape " \ ' and non printable characters using the rules of literal C strings and characters + # Escape `"` `\` `'`, trigraphs and non printable characters using the rules of literal C strings and characters # - # assert "abAB12<>&".escape_to_c == "abAB12<>&" - # assert "\n\"'\\".escape_to_c == "\\n\\\"\\'\\\\" + # ~~~ + # assert "abAB12<>&".escape_to_c == "abAB12<>&" + # assert "\n\"'\\".escape_to_c == "\\n\\\"\\'\\\\" + # assert "allo???!".escape_to_c == "allo??\\?!" + # assert "??=??/??'??(??)".escape_to_c == "?\\?=?\\?/??\\'?\\?(?\\?)" + # assert "??!????-".escape_to_c == "?\\?!?\\??\\?-" + # ~~~ # # Most non-printable characters (bellow ASCII 32) are escaped to an octal form `\nnn`. # Three digits are always used to avoid following digits to be interpreted as an element # of the octal sequence. # - # assert "{0.ascii}{1.ascii}{8.ascii}{31.ascii}{32.ascii}".escape_to_c == "\\000\\001\\010\\037 " + # ~~~ + # assert "{0.code_point}{1.code_point}{8.code_point}{31.code_point}{32.code_point}".escape_to_c == "\\000\\001\\010\\037 " + # ~~~ # # The exceptions are the common `\t` and `\n`. fun escape_to_c: String @@ -605,9 +695,27 @@ abstract class Text b.append("\\\'") else if c == '\\' then b.append("\\\\") - else if c.ascii < 32 then + else if c == '?' then + # Escape if it is the last question mark of a ANSI C trigraph. + var j = i + 1 + if j < length then + var next = chars[j] + # We ignore `??'` because it will be escaped as `??\'`. + if + next == '!' or + next == '(' or + next == ')' or + next == '-' or + next == '/' or + next == '<' or + next == '=' or + next == '>' + then b.add('\\') + end + b.add('?') + else if c.code_point < 32 then b.add('\\') - var oct = c.ascii.to_base(8, false) + var oct = c.code_point.to_base(8) # Force 3 octal digits since it is the # maximum allowed in the C specification if oct.length == 1 then @@ -627,7 +735,10 @@ abstract class Text # Escape additionnal characters # The result might no be legal in C but be used in other languages # - # assert "ab|\{\}".escape_more_to_c("|\{\}") == "ab\\|\\\{\\\}" + # ~~~ + # assert "ab|\{\}".escape_more_to_c("|\{\}") == "ab\\|\\\{\\\}" + # assert "allo???!".escape_more_to_c("") == "allo??\\?!" + # ~~~ fun escape_more_to_c(chars: String): String do var b = new Buffer @@ -642,14 +753,18 @@ abstract class Text # Escape to C plus braces # - # assert "\n\"'\\\{\}".escape_to_nit == "\\n\\\"\\'\\\\\\\{\\\}" + # ~~~ + # assert "\n\"'\\\{\}".escape_to_nit == "\\n\\\"\\'\\\\\\\{\\\}" + # ~~~ fun escape_to_nit: String do return escape_more_to_c("\{\}") # Escape to POSIX Shell (sh). # # Abort if the text contains a null byte. # - # assert "\n\"'\\\{\}0".escape_to_sh == "'\n\"'\\''\\\{\}0'" + # ~~~ + # assert "\n\"'\\\{\}0".escape_to_sh == "'\n\"'\\''\\\{\}0'" + # ~~~ fun escape_to_sh: String do var b = new Buffer b.chars.add '\'' @@ -680,8 +795,8 @@ abstract class Text else if c == ':' or c == ' ' or c == '#' then b.add('\\') b.add(c) - else if c.ascii < 32 or c == ';' or c == '|' or c == '\\' or c == '=' then - b.append("?{c.ascii.to_base(16, false)}") + else if c.code_point < 32 or c == ';' or c == '|' or c == '\\' then + b.append("?{c.code_point.to_base(16)}") else b.add(c) end @@ -691,11 +806,13 @@ abstract class Text # Return a string where Nit escape sequences are transformed. # - # var s = "\\n" - # assert s.length == 2 - # var u = s.unescape_nit - # assert u.length == 1 - # assert u.chars[0].ascii == 10 # (the ASCII value of the "new line" character) + # ~~~ + # var s = "\\n" + # assert s.length == 2 + # var u = s.unescape_nit + # assert u.length == 1 + # assert u.chars[0].code_point == 10 # (the ASCII value of the "new line" character) + # ~~~ fun unescape_nit: String do var res = new Buffer.with_cap(self.length) @@ -726,12 +843,63 @@ 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).to_u32 + if cp < 0xD800u32 then return cp.code_point + if cp > 0xDFFFu32 then return cp.code_point + if cp > 0xDBFFu32 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).to_u32 + var cplo = cp & 0xFFFFu32 + if cplo < 0xDC00u32 then return 0xFFFD.code_point + if cplo > 0xDFFFu32 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-._~" - # assert "%()< >".to_percent_encoding == "%25%28%29%3c%20%3e" - # assert ".com/post?e=asdf&f=123".to_percent_encoding == ".com%2fpost%3fe%3dasdf%26f%3d123" - # assert "éあいう".to_percent_encoding == "%c3%a9%e3%81%82%e3%81%84%e3%81%86" + # ~~~ + # assert "aBc09-._~".to_percent_encoding == "aBc09-._~" + # assert "%()< >".to_percent_encoding == "%25%28%29%3c%20%3e" + # assert ".com/post?e=asdf&f=123".to_percent_encoding == ".com%2fpost%3fe%3dasdf%26f%3d123" + # assert "éあいう".to_percent_encoding == "%c3%a9%e3%81%82%e3%81%84%e3%81%86" + # ~~~ fun to_percent_encoding: String do var buf = new Buffer @@ -756,18 +924,21 @@ abstract class Text # Decode `self` from percent (or URL) encoding to a clear string # - # Replace invalid use of '%' with '?'. + # Invalid '%' are not decoded. # - # assert "aBc09-._~".from_percent_encoding == "aBc09-._~" - # assert "%25%28%29%3c%20%3e".from_percent_encoding == "%()< >" - # assert ".com%2fpost%3fe%3dasdf%26f%3d123".from_percent_encoding == ".com/post?e=asdf&f=123" - # assert "%25%28%29%3C%20%3E".from_percent_encoding == "%()< >" - # assert "incomplete %".from_percent_encoding == "incomplete ?" - # assert "invalid % usage".from_percent_encoding == "invalid ? usage" - # assert "%c3%a9%e3%81%82%e3%81%84%e3%81%86".from_percent_encoding == "éあいう" + # ~~~ + # assert "aBc09-._~".from_percent_encoding == "aBc09-._~" + # assert "%25%28%29%3c%20%3e".from_percent_encoding == "%()< >" + # assert ".com%2fpost%3fe%3dasdf%26f%3d123".from_percent_encoding == ".com/post?e=asdf&f=123" + # assert "%25%28%29%3C%20%3E".from_percent_encoding == "%()< >" + # assert "incomplete %".from_percent_encoding == "incomplete %" + # assert "invalid % usage".from_percent_encoding == "invalid % usage" + # assert "%c3%a9%e3%81%82%e3%81%84%e3%81%86".from_percent_encoding == "éあいう" + # assert "%1 %A %C3%A9A9".from_percent_encoding == "%1 %A éA9" + # ~~~ fun from_percent_encoding: String do - var len = bytelen + var len = byte_length var has_percent = false for c in chars do if c == '%' then @@ -779,7 +950,7 @@ abstract class Text # If no transformation is needed, return self as a string if not has_percent then return to_s - var buf = new NativeString(len) + var buf = new CString(len) var i = 0 var l = 0 while i < length do @@ -787,74 +958,90 @@ abstract class Text if c == '%' then if i + 2 >= length then # What follows % has been cut off - buf[l] = '?'.ascii.to_b + buf[l] = u'%' else i += 1 var hex_s = substring(i, 2) if hex_s.is_hex then var hex_i = hex_s.to_hex - buf[l] = hex_i.to_b + buf[l] = hex_i i += 1 else # What follows a % is not Hex - buf[l] = '?'.ascii.to_b + buf[l] = u'%' i -= 1 end end - else buf[l] = c.ascii.to_b + else buf[l] = c.code_point i += 1 l += 1 end - return buf.to_s_with_length(l) + return buf.to_s_unsafe(l, copy=false) end # Escape the characters `<`, `>`, `&`, `"`, `'` and `/` as HTML/XML entity references. # - # assert "a&b-<>\"x\"/'".html_escape == "a&b-<>"x"/'" + # ~~~ + # assert "a&b-<>\"x\"/'".html_escape == "a&b-<>"x"/'" + # ~~~ # # SEE: fun html_escape: String do - var buf = new Buffer + var buf: nullable Buffer = null for i in [0..length[ do var c = chars[i] + var sub = null if c == '&' then - buf.append "&" + sub = "&" else if c == '<' then - buf.append "<" + sub = "<" else if c == '>' then - buf.append ">" + sub = ">" else if c == '"' then - buf.append """ + sub = """ else if c == '\'' then - buf.append "'" + sub = "'" else if c == '/' then - buf.append "/" - else buf.add c + sub = "/" + else + if buf != null then buf.add c + continue + end + if buf == null then + buf = new Buffer + for j in [0..i[ do buf.add chars[j] + end + buf.append sub end + if buf == null then return self.to_s return buf.to_s end # Equality of text # Two pieces of text are equals if thez have the same characters in the same order. # - # assert "hello" == "hello" - # assert "hello" != "HELLO" - # assert "hello" == "hel"+"lo" + # ~~~ + # assert "hello" == "hello" + # assert "hello" != "HELLO" + # assert "hello" == "hel"+"lo" + # ~~~ # # Things that are not Text are not equal. # - # assert "9" != '9' - # assert "9" != ['9'] - # assert "9" != 9 + # ~~~ + # assert "9" != '9' + # assert "9" != ['9'] + # assert "9" != 9 # - # assert "9".chars.first == '9' # equality of Char - # assert "9".chars == ['9'] # equality of Sequence - # assert "9".to_i == 9 # equality of Int + # assert "9".chars.first == '9' # equality of Char + # assert "9".chars == ['9'] # equality of Sequence + # assert "9".to_i == 9 # equality of Int + # ~~~ redef fun ==(o) do if o == null then return false @@ -866,8 +1053,10 @@ abstract class Text # Lexicographical comparaison # - # assert "abc" < "xy" - # assert "ABC" < "abc" + # ~~~ + # assert "abc" < "xy" + # assert "ABC" < "abc" + # ~~~ redef fun <(other) do var self_chars = self.chars.iterator @@ -889,7 +1078,9 @@ abstract class Text # Escape string used in labels for graphviz # - # assert ">><<".escape_to_dot == "\\>\\>\\<\\<" + # ~~~ + # assert ">><<".escape_to_dot == "\\>\\>\\<\\<" + # ~~~ fun escape_to_dot: String do return escape_more_to_c("|\{\}<>") @@ -905,7 +1096,7 @@ abstract class Text for i in [0..length[ do var char = chars[i] - h = (h << 5) + h + char.ascii + h = (h << 5) + h + char.code_point end hash_cache = h @@ -913,36 +1104,48 @@ 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 @@ -950,17 +1153,77 @@ 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 + # Basically a high-level synonym of CString::copy_to # # REQUIRE: `n` must be large enough to contain `len` bytes # - # 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" - # - fun copy_to_native(dest: NativeString, n, src_offset, dest_offset: Int) do + # ~~~ + # var ns = new CString(8) + # "Text is String".copy_to_native(ns, 8, 2, 0) + # assert ns.to_s_with_length(8) == "xt is St" + # ~~~ + fun copy_to_native(dest: CString, n, src_offset, dest_offset: Int) do var mypos = src_offset var itspos = dest_offset while n > 0 do @@ -971,266 +1234,284 @@ abstract class Text end end -end - -# All kinds of array-based text representations. -abstract class FlatText - super Text - - # Underlying C-String (`char*`) - # - # Warning : Might be void in some subclasses, be sure to check - # if set before using it. - private var items: NativeString is noinit - - # Real items, used as cache for to_cstring is called - private var real_items: nullable NativeString = null - - # Returns a char* starting at position `first_byte` - # - # WARNING: If you choose to use this service, be careful of the following. - # - # Strings and NativeString are *ideally* always allocated through a Garbage Collector. - # Since the GC tracks the use of the pointer for the beginning of the char*, it may be - # deallocated at any moment, rendering the pointer returned by this function invalid. - # Any access to freed memory may very likely cause undefined behaviour or a crash. - # (Failure to do so will most certainly result in long and painful debugging hours) - # - # The only safe use of this pointer is if it is ephemeral (e.g. read in a C function - # then immediately return). + # Packs the content of a string in packs of `ln` chars. + # This variant ensures that only the last element might be smaller than `ln` # - # As always, do not modify the content of the String in C code, if this is what you want - # copy locally the char* as Nit Strings are immutable. - private fun fast_cstring: NativeString is abstract - - redef var length = 0 - - redef var bytelen = 0 - - redef fun output - do - var i = 0 - while i < length do - items[i].output - i += 1 + # ~~~ + # var s = "abcdefghijklmnopqrstuvwxyz" + # assert s.pack_l(4) == ["abcd","efgh","ijkl","mnop","qrst","uvwx","yz"] + # ~~~ + fun pack_l(ln: Int): Array[Text] do + var st = 0 + var retarr = new Array[Text].with_capacity(length / ln + length % ln) + while st < length do + retarr.add(substring(st, ln)) + st += ln end + return retarr end - redef fun copy_to_native(dest, n, src_offset, dest_offset) do - items.copy_to(dest, n, src_offset, dest_offset) + # Packs the content of a string in packs of `ln` chars. + # This variant ensures that only the first element might be smaller than `ln` + # + # ~~~ + # var s = "abcdefghijklmnopqrstuvwxyz" + # assert s.pack_r(4) == ["ab","cdef","ghij","klmn","opqr","stuv","wxyz"] + # ~~~ + fun pack_r(ln: Int): Array[Text] do + var st = length + var retarr = new Array[Text].with_capacity(length / ln + length % ln) + while st >= 0 do + retarr.add(substring(st - ln, ln)) + st -= ln + end + return retarr.reversed end -end -# Abstract class for the SequenceRead compatible -# views on the chars of any Text -private abstract class StringCharView - super SequenceRead[Char] + # Concatenates self `i` times + # + # ~~~ + # assert "abc" * 4 == "abcabcabcabc" + # assert "abc" * 1 == "abc" + # assert "abc" * 0 == "" + # var b = new Buffer + # b.append("天地") + # b = b * 4 + # assert b == "天地天地天地天地" + # ~~~ + fun *(i: Int): SELFTYPE is abstract - type SELFTYPE: Text + # Insert `s` at `pos`. + # + # ~~~ + # assert "helloworld".insert_at(" ", 5) == "hello world" + # var b = new Buffer + # b.append("Hello世界") + # b = b.insert_at(" beautiful ", 5) + # assert b == "Hello beautiful 世界" + # ~~~ + fun insert_at(s: String, pos: Int): SELFTYPE is abstract - var target: SELFTYPE + # Returns a reversed version of self + # + # ~~~ + # assert "hello".reversed == "olleh" + # assert "bob".reversed == "bob" + # assert "".reversed == "" + # ~~~ + fun reversed: SELFTYPE is abstract - redef fun is_empty do return target.is_empty + # A upper case version of `self` + # + # ~~~ + # assert "Hello World!".to_upper == "HELLO WORLD!" + # ~~~ + fun to_upper: SELFTYPE is abstract - redef fun length do return target.length - - redef fun iterator: IndexedIterator[Char] do return self.iterator_from(0) - - redef fun reverse_iterator do return self.reverse_iterator_from(self.length - 1) -end - -# Abstract class for the SequenceRead compatible -# views on the bytes of any Text -private abstract class StringByteView - super SequenceRead[Byte] - - type SELFTYPE: Text - - var target: SELFTYPE - - redef fun is_empty do return target.is_empty - - redef fun length do return target.length - - redef fun iterator do return self.iterator_from(0) - - redef fun reverse_iterator do return self.reverse_iterator_from(target.bytelen - 1) -end - -# Immutable sequence of characters. -# -# String objects may be created using literals. -# -# assert "Hello World!" isa String -abstract class String - super Text - - redef type SELFTYPE: String is fixed - - redef fun to_s do return self - - # Concatenates `o` to `self` - # - # assert "hello" + "world" == "helloworld" - # assert "" + "hello" + "" == "hello" - fun +(o: Text): SELFTYPE is abstract - - # Concatenates self `i` times - # - # assert "abc" * 4 == "abcabcabcabc" - # assert "abc" * 1 == "abc" - # assert "abc" * 0 == "" - fun *(i: Int): SELFTYPE is abstract - - # Insert `s` at `pos`. - # - # assert "helloworld".insert_at(" ", 5) == "hello world" - fun insert_at(s: String, pos: Int): SELFTYPE is abstract - - redef fun substrings is abstract - - # Returns a reversed version of self - # - # assert "hello".reversed == "olleh" - # assert "bob".reversed == "bob" - # assert "".reversed == "" - fun reversed: SELFTYPE is abstract - - # A upper case version of `self` - # - # assert "Hello World!".to_upper == "HELLO WORLD!" - fun to_upper: SELFTYPE is abstract - - # A lower case version of `self` - # - # assert "Hello World!".to_lower == "hello world!" - fun to_lower : SELFTYPE is abstract + # A lower case version of `self` + # + # ~~~ + # assert "Hello World!".to_lower == "hello world!" + # ~~~ + fun to_lower : SELFTYPE is abstract # Takes a camel case `self` and converts it to snake case # - # assert "randomMethodId".to_snake_case == "random_method_id" + # ~~~ + # assert "randomMethodId".to_snake_case == "random_method_id" + # ~~~ # # The rules are the following: # # An uppercase is always converted to a lowercase # - # assert "HELLO_WORLD".to_snake_case == "hello_world" + # ~~~ + # assert "HELLO_WORLD".to_snake_case == "hello_world" + # ~~~ # # An uppercase that follows a lowercase is prefixed with an underscore # - # assert "HelloTheWORLD".to_snake_case == "hello_the_world" + # ~~~ + # assert "HelloTheWORLD".to_snake_case == "hello_the_world" + # ~~~ # # An uppercase that follows an uppercase and is followed by a lowercase, is prefixed with an underscore # - # assert "HelloTHEWorld".to_snake_case == "hello_the_world" + # ~~~ + # assert "HelloTHEWorld".to_snake_case == "hello_the_world" + # ~~~ # # All other characters are kept as is; `self` does not need to be a proper CamelCased string. # - # assert "=-_H3ll0Th3W0rld_-=".to_snake_case == "=-_h3ll0th3w0rld_-=" - fun to_snake_case: SELFTYPE - do - if self.is_lower then return self - - var new_str = new Buffer.with_cap(self.length) - var prev_is_lower = false - var prev_is_upper = false - - for i in [0..length[ do - var char = chars[i] - if char.is_lower then - new_str.add(char) - prev_is_lower = true - prev_is_upper = false - else if char.is_upper then - if prev_is_lower then - new_str.add('_') - else if prev_is_upper and i+1 < length and chars[i+1].is_lower then - new_str.add('_') - end - new_str.add(char.to_lower) - prev_is_lower = false - prev_is_upper = true - else - new_str.add(char) - prev_is_lower = false - prev_is_upper = false - end - end - - return new_str.to_s - end + # ~~~ + # assert "=-_H3ll0Th3W0rld_-=".to_snake_case == "=-_h3ll0th3w0rld_-=" + # ~~~ + fun to_snake_case: SELFTYPE is abstract # Takes a snake case `self` and converts it to camel case # - # assert "random_method_id".to_camel_case == "randomMethodId" + # ~~~ + # assert "random_method_id".to_camel_case == "randomMethodId" + # ~~~ # # If the identifier is prefixed by an underscore, the underscore is ignored # - # assert "_private_field".to_camel_case == "_privateField" + # ~~~ + # assert "_private_field".to_camel_case == "_privateField" + # ~~~ # # If `self` is upper, it is returned unchanged # - # assert "RANDOM_ID".to_camel_case == "RANDOM_ID" + # ~~~ + # assert "RANDOM_ID".to_camel_case == "RANDOM_ID" + # ~~~ # # If there are several consecutive underscores, they are considered as a single one # - # assert "random__method_id".to_camel_case == "randomMethodId" - fun to_camel_case: SELFTYPE - do - if self.is_upper then return self - - var new_str = new Buffer - var is_first_char = true - var follows_us = false - - for i in [0..length[ do - var char = chars[i] - if is_first_char then - new_str.add(char) - is_first_char = false - else if char == '_' then - follows_us = true - else if follows_us then - new_str.add(char.to_upper) - follows_us = false - else - new_str.add(char) - end - end - - return new_str.to_s - end + # ~~~ + # assert "random__method_id".to_camel_case == "randomMethodId" + # ~~~ + fun to_camel_case: SELFTYPE is abstract # Returns a capitalized `self` # # Letters that follow a letter are lowercased # Letters that follow a non-letter are upcased. # + # If `keep_upper = true`, already uppercase letters are not lowercased. + # # SEE : `Char::is_letter` for the definition of letter. # - # assert "jAVASCRIPT".capitalized == "Javascript" - # assert "i am root".capitalized == "I Am Root" - # assert "ab_c -ab0c ab\nc".capitalized == "Ab_C -Ab0C Ab\nC" - fun capitalized: SELFTYPE do + # ~~~ + # assert "jAVASCRIPT".capitalized == "Javascript" + # assert "i am root".capitalized == "I Am Root" + # assert "ab_c -ab0c ab\nc".capitalized == "Ab_C -Ab0C Ab\nC" + # assert "preserve my ACRONYMS".capitalized(keep_upper=true) == "Preserve My ACRONYMS" + # ~~~ + fun capitalized(keep_upper: nullable Bool): SELFTYPE do if length == 0 then return self var buf = new Buffer.with_cap(length) + buf.capitalize(keep_upper=keep_upper, src=self) + return buf.to_s + end +end - var curr = chars[0].to_upper - var prev = curr - buf[0] = curr +# All kinds of array-based text representations. +abstract class FlatText + super Text - for i in [1 .. length[ do - prev = curr - curr = self[i] - if prev.is_letter then - buf[i] = curr.to_lower - else - buf[i] = curr.to_upper - end + # Underlying CString (`char*`) + # + # Warning: Might be void in some subclasses, be sure to check + # if set before using it. + var items: CString is noinit + + # Returns a char* starting at position `first_byte` + # + # WARNING: If you choose to use this service, be careful of the following. + # + # Strings and CString are *ideally* always allocated through a Garbage Collector. + # Since the GC tracks the use of the pointer for the beginning of the char*, it may be + # deallocated at any moment, rendering the pointer returned by this function invalid. + # Any access to freed memory may very likely cause undefined behaviour or a crash. + # (Failure to do so will most certainly result in long and painful debugging hours) + # + # The only safe use of this pointer is if it is ephemeral (e.g. read in a C function + # then immediately return). + # + # As always, do not modify the content of the String in C code, if this is what you want + # copy locally the char* as Nit Strings are immutable. + fun fast_cstring: CString is abstract + + redef var length = 0 + + redef var byte_length = 0 + + redef fun output + do + var i = 0 + while i < length do + items[i].output + i += 1 end + end - return buf.to_s + redef fun copy_to_native(dest, n, src_offset, dest_offset) do + items.copy_to(dest, n, src_offset, dest_offset) + end +end + +# Abstract class for the SequenceRead compatible +# views on the chars of any Text +private abstract class StringCharView + super SequenceRead[Char] + + type SELFTYPE: Text + + var target: SELFTYPE + + redef fun is_empty do return target.is_empty + + redef fun length do return target.length + + redef fun iterator: IndexedIterator[Char] do return self.iterator_from(0) + + redef fun reverse_iterator do return self.reverse_iterator_from(self.length - 1) +end + +# Abstract class for the SequenceRead compatible +# views on the bytes of any Text +private abstract class StringByteView + super SequenceRead[Int] + + type SELFTYPE: Text + + var target: SELFTYPE + + redef fun is_empty do return target.is_empty + + redef fun length do return target.byte_length + + redef fun iterator do return self.iterator_from(0) + + redef fun reverse_iterator do return self.reverse_iterator_from(target.byte_length - 1) +end + +# Immutable sequence of characters. +# +# String objects may be created using literals. +# +# ~~~ +# assert "Hello World!" isa String +# ~~~ +abstract class String + super Text + + redef type SELFTYPE: String is fixed + + redef fun to_s do return self + + redef fun clone do return self + + redef fun to_buffer do return new Buffer.from_text(self) + + redef fun to_camel_case do + if self.is_upper then return self + + var new_str = new Buffer.with_cap(length) + new_str.append self + new_str.camel_case + return new_str.to_s + end + + redef fun to_snake_case do + if self.is_lower then return self + + var new_str = new Buffer.with_cap(self.length) + new_str.append self + new_str.snake_case + return new_str.to_s end end @@ -1244,10 +1525,14 @@ abstract class Buffer # Returns an instance of a subclass of `Buffer` with `i` base capacity new with_cap(i: Int) is abstract - redef type SELFTYPE: Buffer is fixed + # Returns an instance of a subclass of `Buffer` with `t` as content + new from_text(t: Text) do + var ret = new Buffer.with_cap(t.byte_length) + ret.append t + return ret + end - # Specific implementations MUST set this to `true` in order to invalidate caches - protected var is_dirty = true + redef type SELFTYPE: Buffer is fixed # Copy-On-Write flag # @@ -1258,22 +1543,34 @@ abstract class Buffer protected var written = false # Modifies the char contained at pos `index` - # - # DEPRECATED : Use self.chars.[]= instead fun []=(index: Int, item: Char) is abstract + redef fun to_buffer do return clone + + # ~~~ + # var b = new Buffer + # b.append("Buffer!") + # var c = b.clone + # assert b == c + # ~~~ + redef fun clone do + var cln = new Buffer.with_cap(byte_length) + cln.append self + return cln + end + # Adds a char `c` at the end of self - # - # DEPRECATED : Use self.chars.add instead fun add(c: Char) is abstract # Clears the buffer # - # var b = new Buffer - # b.append "hello" - # assert not b.is_empty - # b.clear - # assert b.is_empty + # ~~~ + # var b = new Buffer + # b.append "hello" + # assert not b.is_empty + # b.clear + # assert b.is_empty + # ~~~ fun clear is abstract # Enlarges the subsequent array containing the chars of self @@ -1281,42 +1578,52 @@ abstract class Buffer # Adds the content of text `s` at the end of self # - # var b = new Buffer - # b.append "hello" - # b.append "world" - # assert b == "helloworld" + # ~~~ + # var b = new Buffer + # b.append "hello" + # b.append "world" + # assert b == "helloworld" + # ~~~ fun append(s: Text) is abstract # `self` is appended in such a way that `self` is repeated `r` times # - # var b = new Buffer - # b.append "hello" - # b.times 3 - # assert b == "hellohellohello" + # ~~~ + # var b = new Buffer + # b.append "hello" + # b.times 3 + # assert b == "hellohellohello" + # ~~~ fun times(r: Int) is abstract # Reverses itself in-place # - # var b = new Buffer - # b.append("hello") - # b.reverse - # assert b == "olleh" + # ~~~ + # var b = new Buffer + # b.append("hello") + # b.reverse + # assert b == "olleh" + # ~~~ fun reverse is abstract # Changes each lower-case char in `self` by its upper-case variant # - # var b = new Buffer - # b.append("Hello World!") - # b.upper - # assert b == "HELLO WORLD!" + # ~~~ + # var b = new Buffer + # b.append("Hello World!") + # b.upper + # assert b == "HELLO WORLD!" + # ~~~ fun upper is abstract # Changes each upper-case char in `self` by its lower-case variant # - # var b = new Buffer - # b.append("Hello World!") - # b.lower - # assert b == "hello world!" + # ~~~ + # var b = new Buffer + # b.append("Hello World!") + # b.lower + # assert b == "hello world!" + # ~~~ fun lower is abstract # Capitalizes each word in `self` @@ -1324,42 +1631,238 @@ abstract class Buffer # Letters that follow a letter are lowercased # Letters that follow a non-letter are upcased. # + # If `keep_upper = true`, uppercase letters are not lowercased. + # + # When `src` is specified, this method reads from `src` instead of `self` + # but it still writes the result to the beginning of `self`. + # This requires `self` to have the capacity to receive all of the + # capitalized content of `src`. + # # SEE: `Char::is_letter` for the definition of a letter. # - # var b = new FlatBuffer.from("jAVAsCriPt") - # b.capitalize - # assert b == "Javascript" - # b = new FlatBuffer.from("i am root") - # b.capitalize - # assert b == "I Am Root" - # b = new FlatBuffer.from("ab_c -ab0c ab\nc") - # b.capitalize - # assert b == "Ab_C -Ab0C Ab\nC" - fun capitalize do + # ~~~ + # var b = new FlatBuffer.from("jAVAsCriPt") + # b.capitalize + # assert b == "Javascript" + # b = new FlatBuffer.from("i am root") + # b.capitalize + # assert b == "I Am Root" + # b = new FlatBuffer.from("ab_c -ab0c ab\nc") + # b.capitalize + # assert b == "Ab_C -Ab0C Ab\nC" + # + # b = new FlatBuffer.from("12345") + # b.capitalize(src="foo") + # assert b == "Foo45" + # + # b = new FlatBuffer.from("preserve my ACRONYMS") + # b.capitalize(keep_upper=true) + # assert b == "Preserve My ACRONYMS" + # ~~~ + fun capitalize(keep_upper: nullable Bool, src: nullable Text) do + src = src or else self + var length = src.length if length == 0 then return - var c = self[0].to_upper + keep_upper = keep_upper or else false + + var c = src[0].to_upper self[0] = c var prev = c for i in [1 .. length[ do prev = c - c = self[i] + c = src[i] if prev.is_letter then - self[i] = c.to_lower + if keep_upper then + self[i] = c + else + self[i] = c.to_lower + end else self[i] = c.to_upper end end end - redef fun hash - do - if is_dirty then hash_cache = null - return super - end - # In Buffers, the internal sequence of character is mutable # Thus, `chars` can be used to modify the buffer. redef fun chars: Sequence[Char] is abstract + + # Appends `length` chars from `s` starting at index `from` + # + # ~~~ + # var b = new Buffer + # b.append_substring("abcde", 1, 2) + # assert b == "bc" + # b.append_substring("vwxyz", 2, 3) + # assert b == "bcxyz" + # b.append_substring("ABCDE", 4, 300) + # assert b == "bcxyzE" + # b.append_substring("VWXYZ", 400, 1) + # assert b == "bcxyzE" + # ~~~ + fun append_substring(s: Text, from, length: Int) do + if from < 0 then + length += from + from = 0 + end + var ln = s.length + if (length + from) > ln then length = ln - from + if length <= 0 then return + append_substring_impl(s, from, length) + end + + # Unsafe version of `append_substring` for performance + # + # NOTE: Use only if sure about `from` and `length`, no checks + # or bound recalculation is done + fun append_substring_impl(s: Text, from, length: Int) do + var max = from + length + for i in [from .. max[ do add s[i] + end + + redef fun *(i) do + var ret = new Buffer.with_cap(byte_length * i) + for its in [0 .. i[ do ret.append self + return ret + end + + redef fun insert_at(s, pos) do + var obuf = new Buffer.with_cap(byte_length + s.byte_length) + obuf.append_substring(self, 0, pos) + obuf.append s + obuf.append_substring(self, pos, length - pos) + return obuf + end + + # Inserts `s` at position `pos` + # + # ~~~ + # var b = new Buffer + # b.append "美しい世界" + # b.insert(" nit ", 3) + # assert b == "美しい nit 世界" + # ~~~ + fun insert(s: Text, pos: Int) is abstract + + # Inserts `c` at position `pos` + # + # ~~~ + # var b = new Buffer + # b.append "美しい世界" + # b.insert_char(' ', 3) + # assert b == "美しい 世界" + # ~~~ + fun insert_char(c: Char, pos: Int) is abstract + + # Removes a substring from `self` at position `pos` + # + # NOTE: `length` defaults to 1, expressed in chars + # + # ~~~ + # var b = new Buffer + # b.append("美しい 世界") + # b.remove_at(3) + # assert b == "美しい世界" + # b.remove_at(1, 2) + # assert b == "美世界" + # ~~~ + fun remove_at(pos: Int, length: nullable Int) is abstract + + redef fun reversed do + var ret = clone + ret.reverse + return ret + end + + redef fun to_upper do + var ret = clone + ret.upper + return ret + end + + redef fun to_lower do + var ret = clone + ret.lower + return ret + end + + redef fun to_snake_case do + var ret = clone + ret.snake_case + return ret + end + + # Takes a camel case `self` and converts it to snake case + # + # SEE: `to_snake_case` + fun snake_case do + if self.is_lower then return + var prev_is_lower = false + var prev_is_upper = false + + var i = 0 + while i < length do + var char = chars[i] + if char.is_lower then + prev_is_lower = true + prev_is_upper = false + else if char.is_upper then + if prev_is_lower then + insert_char('_', i) + i += 1 + else if prev_is_upper and i + 1 < length and self[i + 1].is_lower then + insert_char('_', i) + i += 1 + end + self[i] = char.to_lower + prev_is_lower = false + prev_is_upper = true + else + prev_is_lower = false + prev_is_upper = false + end + i += 1 + end + end + + redef fun to_camel_case + do + var new_str = clone + new_str.camel_case + return new_str + end + + # Takes a snake case `self` and converts it to camel case + # + # SEE: `to_camel_case` + fun camel_case do + if is_upper then return + + var underscore_count = 0 + + var pos = 1 + while pos < length do + var char = self[pos] + if char == '_' then + underscore_count += 1 + else if underscore_count > 0 then + pos -= underscore_count + remove_at(pos, underscore_count) + self[pos] = char.to_upper + underscore_count = 0 + end + pos += 1 + end + if underscore_count > 0 then remove_at(pos - underscore_count - 1, underscore_count) + end + + redef fun capitalized(keep_upper) do + if length == 0 then return self + + var buf = new Buffer.with_cap(byte_length) + buf.capitalize(keep_upper=keep_upper, src=self) + return buf + end end # View for chars on Buffer objects, extends Sequence @@ -1384,12 +1887,14 @@ redef class Object # User readable representation of `self`. fun to_s: String do return inspect - # The class name of the object in NativeString format. - private fun native_class_name: NativeString is intern + # The class name of the object in CString format. + private fun native_class_name: CString is intern # The class name of the object. # - # assert 5.class_name == "Int" + # ~~~ + # assert 5.class_name == "Int" + # ~~~ fun class_name: String do return native_class_name.to_s # Developer readable representation of `self`. @@ -1408,8 +1913,10 @@ redef class Object end redef class Bool - # assert true.to_s == "true" - # assert false.to_s == "false" + # ~~~ + # assert true.to_s == "true" + # assert false.to_s == "false" + # ~~~ redef fun to_s do if self then @@ -1421,40 +1928,42 @@ redef class Bool end redef class Byte - # C function to calculate the length of the `NativeString` to receive `self` + # C function to calculate the length of the `CString` to receive `self` private fun byte_to_s_len: Int `{ return snprintf(NULL, 0, "0x%02x", self); `} - # C function to convert an nit Int to a NativeString (char*) - private fun native_byte_to_s(nstr: NativeString, strlen: Int) `{ + # C function to convert an nit Int to a CString (char*) + private fun native_byte_to_s(nstr: CString, strlen: Int) `{ snprintf(nstr, strlen, "0x%02x", self); `} # Displayable byte in its hexadecimal form (0x..) # - # assert 1.to_b.to_s == "0x01" - # assert (-123).to_b.to_s == "0x85" + # ~~~ + # assert 1.to_b.to_s == "0x01" + # assert (-123).to_b.to_s == "0x85" + # ~~~ redef fun to_s do var nslen = byte_to_s_len - var ns = new NativeString(nslen + 1) - ns[nslen] = 0u8 + var ns = new CString(nslen + 1) + ns[nslen] = 0 native_byte_to_s(ns, nslen + 1) - return ns.to_s_with_length(nslen) + return ns.to_s_unsafe(nslen, copy=false, clean=false) end end redef class Int # Wrapper of strerror C function - private fun strerror_ext: NativeString `{ return strerror(self); `} + private fun strerror_ext: CString `{ return strerror((int)self); `} # Returns a string describing error number fun strerror: String do return strerror_ext.to_s - # Fill `s` with the digits in base `base` of `self` (and with the '-' sign if 'signed' and negative). + # Fill `s` with the digits in base `base` of `self` (and with the '-' sign if negative). # assume < to_c max const of char - private fun fill_buffer(s: Buffer, base: Int, signed: Bool) + private fun fill_buffer(s: Buffer, base: Int) do var n: Int # Sign @@ -1476,60 +1985,110 @@ redef class Int end end - # C function to calculate the length of the `NativeString` to receive `self` + # C function to calculate the length of the `CString` to receive `self` private fun int_to_s_len: Int `{ return snprintf(NULL, 0, "%ld", self); `} - # C function to convert an nit Int to a NativeString (char*) - private fun native_int_to_s(nstr: NativeString, strlen: Int) `{ + # C function to convert an nit Int to a CString (char*) + private fun native_int_to_s(nstr: CString, strlen: Int) `{ snprintf(nstr, strlen, "%ld", self); `} - # return displayable int in base base and signed - fun to_base(base: Int, signed: Bool): String is abstract + # String representation of `self` in the given `base` + # + # ~~~ + # assert 15.to_base(10) == "15" + # assert 15.to_base(16) == "f" + # assert 15.to_base(2) == "1111" + # assert (-10).to_base(3) == "-101" + # ~~~ + fun to_base(base: Int): String + do + var l = digit_count(base) + var s = new Buffer + s.enlarge(l) + for x in [0..l[ do s.add(' ') + fill_buffer(s, base) + return s.to_s + end + # return displayable int in hexadecimal # - # assert 1.to_hex == "1" - # assert (-255).to_hex == "-ff" - fun to_hex: String do return to_base(16,false) + # ~~~ + # assert 1.to_hex == "1" + # assert (-255).to_hex == "-ff" + # ~~~ + fun to_hex: String do return to_base(16) end redef class Float # Pretty representation of `self`, with decimals as needed from 1 to a maximum of 3 # - # assert 12.34.to_s == "12.34" - # assert (-0120.030).to_s == "-120.03" + # ~~~ + # assert 12.34.to_s == "12.34" + # assert (-0120.030).to_s == "-120.03" + # assert (-inf).to_s == "-inf" + # assert (nan).to_s == "nan" + # ~~~ # # see `to_precision` for a custom precision. redef fun to_s do - var str = to_precision( 3 ) - if is_inf != 0 or is_nan then return str - var len = str.length + var str = to_precision(3) + return adapt_number_of_decimal(str, false) + end + + # Return the representation of `self`, with scientific notation + # + # Adpat the number of decimals as needed from 1 to a maximum of 6 + # ~~~ + # assert 12.34.to_sci == "1.234e+01" + # assert 123.45.to_sci.to_f.to_sci == "1.2345e+02" + # assert 0.001234.to_sci == "1.234e-03" + # assert (inf).to_sci == "inf" + # assert (nan).to_sci == "nan" + # ~~~ + fun to_sci: String + do + var is_inf_or_nan = check_inf_or_nan + if is_inf_or_nan != null then return is_inf_or_nan + return adapt_number_of_decimal(return_from_specific_format("%e".to_cstring), true) + end + + # Return the `string_number` with the adapted number of decimal (i.e the fonction remove the useless `0`) + # `is_expo` it's here to specifi if the given `string_number` is in scientific notation + private fun adapt_number_of_decimal(string_number: String, is_expo: Bool): String + do + # check if `self` does not need an adaptation of the decimal + if is_inf != 0 or is_nan then return string_number + var len = string_number.length + var expo_value = "" + var numeric_value = "" for i in [0..len-1] do - var j = len-1-i - var c = str.chars[j] - if c == '0' then - continue - else if c == '.' then - return str.substring( 0, j+2 ) - else - return str.substring( 0, j+1 ) + var j = len - 1 - i + var c = string_number.chars[j] + if not is_expo then + if c == '0' then + continue + else if c == '.' then + numeric_value = string_number.substring( 0, j + 2) + break + else + numeric_value = string_number.substring( 0, j + 1) + break + end + else if c == 'e' then + expo_value = string_number.substring( j, len - 1 ) + is_expo = false end end - return str + return numeric_value + expo_value end - # `String` representation of `self` with the given number of `decimals` - # - # assert 12.345.to_precision(0) == "12" - # assert 12.345.to_precision(3) == "12.345" - # assert (-12.345).to_precision(3) == "-12.345" - # assert (-0.123).to_precision(3) == "-0.123" - # assert 0.999.to_precision(2) == "1.00" - # assert 0.999.to_precision(4) == "0.9990" - fun to_precision(decimals: Int): String + # Return a string representation of `self` in fonction if it is not a number or infinity. + # Return `null` if `self` is not a not a number or an infinity + private fun check_inf_or_nan: nullable String do if is_nan then return "nan" @@ -1539,43 +2098,88 @@ redef class Float else if isinf == -1 then return "-inf" end + return null + end - if decimals == 0 then return self.to_i.to_s - var f = self - for i in [0..decimals[ do f = f * 10.0 - if self > 0.0 then - f = f + 0.5 - else - f = f - 0.5 - end - var i = f.to_i - if i == 0 then return "0." + "0"*decimals - - # Prepare both parts of the float, before and after the "." - var s = i.abs.to_s - var sl = s.length - var p1 - var p2 - if sl > decimals then - # Has something before the "." - p1 = s.substring(0, sl-decimals) - p2 = s.substring(sl-decimals, decimals) - else - p1 = "0" - p2 = "0"*(decimals-sl) + s - end + # `String` representation of `self` with the given number of `decimals` + # + # ~~~ + # assert 12.345.to_precision(0) == "12" + # assert 12.345.to_precision(3) == "12.345" + # assert (-12.345).to_precision(3) == "-12.345" + # assert (-0.123).to_precision(3) == "-0.123" + # assert 0.999.to_precision(2) == "1.00" + # assert 0.999.to_precision(4) == "0.9990" + # ~~~ + fun to_precision(decimals: Int): String + do + var is_inf_or_nan = check_inf_or_nan + if is_inf_or_nan != null then return is_inf_or_nan + return return_from_specific_format("%.{decimals}f".to_cstring) + end - if i < 0 then p1 = "-" + p1 + # Returns the hexadecimal (`String`) representation of `self` in exponential notation + # + # ~~~ + # assert 12.345.to_hexa_exponential_notation == "0x1.8b0a3d70a3d71p+3" + # assert 12.345.to_hexa_exponential_notation.to_f == 12.345 + # ~~~ + fun to_hexa_exponential_notation: String + do + return return_from_specific_format("%a".to_cstring) + end - return p1 + "." + p2 + # Return the representation of `self`, with the specific given c `format`. + private fun return_from_specific_format(format: CString): String + do + var size = to_precision_size_with_format(format) + var cstr = new CString(size + 1) + to_precision_fill_with_format(format, size + 1, cstr) + return cstr.to_s_unsafe(byte_length = size, copy = false) end + + # The lenght of `self` in the specific given c `format` + private fun to_precision_size_with_format(format: CString): Int`{ + return snprintf(NULL, 0, format, self); + `} + + # Fill `cstr` with `self` in the specific given c `format` + private fun to_precision_fill_with_format(format: CString, size: Int, cstr: CString) `{ + snprintf(cstr, size, format, self); + `} end redef class Char + # Returns a sequence with the UTF-8 bytes of `self` + # + # ~~~ + # assert 'a'.bytes == [0x61] + # assert 'ま'.bytes == [0xE3, 0x81, 0xBE] + # ~~~ + fun bytes: SequenceRead[Int] 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 + + # Is `self` a UTF-16 high surrogate ? + fun is_hi_surrogate: Bool do + var cp = code_point + return cp >= 0xD800 and cp <= 0xDBFF + end + + # Is `self` a UTF-16 low surrogate ? + fun is_lo_surrogate: Bool do + var cp = code_point + return cp >= 0xDC00 and cp <= 0xDFFF + end + # Length of `self` in a UTF-8 String - private fun u8char_len: Int do - var c = self.ascii + fun u8char_len: Int do + var c = self.code_point if c < 0x80 then return 1 if c <= 0x7FF then return 2 if c <= 0xFFFF then return 3 @@ -1584,15 +2188,59 @@ redef class Char return 1 end - # assert 'x'.to_s == "x" + # ~~~ + # assert 'x'.to_s == "x" + # ~~~ redef fun to_s do var ln = u8char_len - var ns = new NativeString(ln + 1) + var ns = new CString(ln + 1) u8char_tos(ns, ln) - return ns.to_s_with_length(ln) + return ns.to_s_unsafe(ln, copy=false, clean=false) end - private fun u8char_tos(r: NativeString, len: Int) `{ + # 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: CString, len: Int) `{ r[len] = '\0'; switch(len){ case 1: @@ -1618,10 +2266,12 @@ redef class Char # Returns true if the char is a numerical digit # - # assert '0'.is_numeric - # assert '9'.is_numeric - # assert not 'a'.is_numeric - # assert not '?'.is_numeric + # ~~~ + # assert '0'.is_numeric + # assert '9'.is_numeric + # assert not 'a'.is_numeric + # assert not '?'.is_numeric + # ~~~ # # FIXME: Works on ASCII-range only fun is_numeric: Bool @@ -1631,10 +2281,12 @@ redef class Char # Returns true if the char is an alpha digit # - # assert 'a'.is_alpha - # assert 'Z'.is_alpha - # assert not '0'.is_alpha - # assert not '?'.is_alpha + # ~~~ + # assert 'a'.is_alpha + # assert 'Z'.is_alpha + # assert not '0'.is_alpha + # assert not '?'.is_alpha + # ~~~ # # FIXME: Works on ASCII-range only fun is_alpha: Bool @@ -1642,19 +2294,46 @@ 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 - # assert 'Z'.is_alphanumeric - # assert '0'.is_alphanumeric - # assert '9'.is_alphanumeric - # assert not '?'.is_alphanumeric + # ~~~ + # assert 'a'.is_alphanumeric + # assert 'Z'.is_alphanumeric + # assert '0'.is_alphanumeric + # assert '9'.is_alphanumeric + # assert not '?'.is_alphanumeric + # ~~~ # # FIXME: Works on ASCII-range only fun is_alphanumeric: Bool 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] @@ -1692,10 +2371,18 @@ redef class Collection[E] # # Only concatenate if `separator == null`. # - # assert [1, 2, 3].join(":") == "1:2:3" - # assert [1..3].join(":") == "1:2:3" - # assert [1..3].join == "123" - fun join(separator: nullable Text): String + # ~~~ + # assert [1, 2, 3].join(":") == "1:2:3" + # assert [1..3].join(":") == "1:2:3" + # assert [1..3].join == "123" + # ~~~ + # + # if `last_separator` is given, then it is used to separate the last element. + # + # ~~~ + # assert [1, 2, 3, 4].join(", ", " and ") == "1, 2, 3 and 4" + # ~~~ + fun join(separator: nullable Text, last_separator: nullable Text): String do if is_empty then return "" @@ -1706,13 +2393,19 @@ redef class Collection[E] var e = i.item if e != null then s.append(e.to_s) + if last_separator == null then last_separator = separator + # Concat other items i.next while i.is_ok do - if separator != null then s.append(separator) e = i.item - if e != null then s.append(e.to_s) i.next + if i.is_ok then + if separator != null then s.append(separator) + else + if last_separator != null then s.append(last_separator) + end + if e != null then s.append(e.to_s) end return s.to_s end @@ -1723,10 +2416,12 @@ redef class Map[K,V] # Key and value are separated by `couple_sep`. # Couples are separated by `sep`. # - # var m = new HashMap[Int, String] - # m[1] = "one" - # m[10] = "ten" - # assert m.join("; ", "=") == "1=one; 10=ten" + # ~~~ + # var m = new HashMap[Int, String] + # m[1] = "one" + # m[10] = "ten" + # assert m.join("; ", "=") == "1=one; 10=ten" + # ~~~ fun join(sep, couple_sep: String): String is abstract end @@ -1763,16 +2458,18 @@ redef class Sys private fun native_argc: Int is intern # Second argument of the main C function. - private fun native_argv(i: Int): NativeString is intern + private fun native_argv(i: Int): CString is intern end # Comparator that efficienlty use `to_s` to compare things # # The comparaison call `to_s` on object and use the result to order things. # -# var a = [1, 2, 3, 10, 20] -# (new CachedAlphaComparator).sort(a) -# assert a == [1, 10, 2, 20, 3] +# ~~~ +# var a = [1, 2, 3, 10, 20] +# (new CachedAlphaComparator).sort(a) +# assert a == [1, 10, 2, 20, 3] +# ~~~ # # Internally the result of `to_s` is cached in a HashMap to counter # uneficient implementation of `to_s`. @@ -1799,7 +2496,12 @@ end # see `alpha_comparator` private class AlphaComparator super Comparator - redef fun compare(a, b) do return a.to_s <=> b.to_s + redef fun compare(a, b) do + if a == b then return 0 + if a == null then return -1 + if b == null then return 1 + return a.to_s <=> b.to_s + end end # Stateless comparator that naively use `to_s` to compare things. @@ -1807,9 +2509,11 @@ end # Note: the result of `to_s` is not cached, thus can be invoked a lot # on a single instace. See `CachedAlphaComparator` as an alternative. # -# var a = [1, 2, 3, 10, 20] -# alpha_comparator.sort(a) -# assert a == [1, 10, 2, 20, 3] +# ~~~ +# var a = [1, 2, 3, 10, 20] +# alpha_comparator.sort(a) +# assert a == [1, 10, 2, 20, 3] +# ~~~ fun alpha_comparator: Comparator do return once new AlphaComparator # The arguments of the program as given by the OS @@ -1818,18 +2522,41 @@ do return sys.program_args end -redef class NativeString - # Returns `self` as a new String. - fun to_s_with_copy: String is abstract +redef class CString - # Returns `self` as a String of `length`. - fun to_s_with_length(length: Int): String is abstract + # Get a `String` from the data at `self` (with unsafe options) + # + # The default behavior is the safest and equivalent to `to_s`. + # + # Options: + # + # * Set `byte_length` to the number of bytes to use as data. + # Otherwise, this method searches for a terminating null byte. + # + # * Set `char_length` to the number of Unicode character in the string. + # Otherwise, the data is read to count the characters. + # Ignored if `clean == true`. + # + # * If `copy == true`, the default, copies the data at `self` in the + # Nit GC allocated memory. Otherwise, the return may still point to + # the data at `self`. + # + # * If `clean == true`, the default, the string is cleaned of invalid UTF-8 + # characters. If cleaning is necessary, the data is copied into Nit GC + # managed memory, whether or not `copy == true`. + # Don't clean only when the data has already been verified as valid UTF-8, + # other library services rely on UTF-8 compliant characters. + fun to_s_unsafe(byte_length, char_length: nullable Int, copy, clean: nullable Bool): String is abstract + + # Retro-compatibility service use by execution engines + # + # TODO remove this method at the next c_src regen. + private fun to_s_full(byte_length, char_length: Int): String do return to_s_unsafe(byte_length, char_length, false, false) - # Returns `self` as a String with `bytelen` and `length` set + # Copies the content of `src` to `self` # - # SEE: `abstract_text::Text` for more infos on the difference - # between `Text::bytelen` and `Text::length` - fun to_s_full(bytelen, unilen: Int): String is abstract + # NOTE: `self` must be large enough to contain `self.byte_length` bytes + fun fill_from(src: Text) do src.copy_to_native(self, src.byte_length, 0, 0) end redef class NativeArray[E]