string: Fix `to_cmangle` to never end with an underscore.
[nit.git] / lib / standard / string.nit
index e3d9d70..c4ae7b0 100644 (file)
@@ -30,7 +30,6 @@ intrude import collection::array
 # High-level abstraction for all text representations
 abstract class Text
        super Comparable
-       super StringCapable
 
        redef type OTHER: Text
 
@@ -181,8 +180,25 @@ abstract class Text
        #
        #     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.`
+       # The first character of `str` being at `pos`, the second
+       # character being at `pos+1` and so on...
+       #
+       # 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
+       #
+       # And that the empty string is always a valid substring.
+       #
+       #     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
+               if pos < 0 or pos + str.length > length then return false
                var myiter = self.chars.iterator_from(pos)
                var itsiter = str.chars.iterator
                while myiter.is_ok and itsiter.is_ok do
@@ -369,13 +385,102 @@ abstract class Text
        #     assert "\na\nb\tc\t".trim          == "a\nb\tc"
        fun trim: SELFTYPE do return (self.l_trim).r_trim
 
-       # Mangle a string to be a unique string only made of alphanumeric characters
+       # Returns `self` removed from its last line terminator (if any).
+       #
+       #    assert "Hello\n".chomp == "Hello"
+       #    assert "Hello".chomp   == "Hello"
+       #
+       #    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"
+       #
+       # Note: unlike with most IO methods like `IStream::read_line`,
+       # a single `\r` is considered here to be a line terminator and will be removed.
+       fun chomp: SELFTYPE
+       do
+               var len = length
+               if len == 0 then return self
+               var l = self.chars.last
+               if l == '\r' then
+                       return substring(0, len-1)
+               else if l != '\n' then
+                       return self
+               else if len > 1 and self.chars[len-2] == '\r' then
+                       return substring(0, len-2)
+               else
+                       return substring(0, len-1)
+               end
+       end
+
+       # Justify a 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)
+       #
+       # Examples
+       #
+       #     assert "hello".justify(10, 0.0)  == "hello     "
+       #     assert "hello".justify(10, 1.0)  == "     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"
+       #
+       # 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): SELFTYPE
+       do
+               var diff = length - self.length
+               if diff <= 0 then return self
+               assert left >= 0.0 and left <= 1.0
+               var before = (diff.to_f * left).to_i
+               return " " * before + self + " " * (diff-before)
+       end
+
+       # Mangle a string to be a unique string only made of alphanumeric characters and underscores.
+       #
+       # This method is injective (two different inputs never produce the same
+       # output) and the returned string always respect the following rules:
+       #
+       # * Contains only US-ASCII letters, digits and underscores.
+       # * Never starts with a digit.
+       # * 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 == ""
        fun to_cmangle: String
        do
+               if is_empty then return ""
                var res = new FlatBuffer
                var underscore = false
-               for i in [0..length[ do
-                       var c = chars[i]
+               var start = 0
+               var c = chars[0]
+
+               if c >= '0' and c <= '9' then
+                       res.add('_')
+                       res.append(c.ascii.to_s)
+                       res.add('d')
+                       start = 1
+               end
+               for i in [start..length[ do
+                       c = chars[i]
                        if (c >= 'a' and c <= 'z') or (c >='A' and c <= 'Z') then
                                res.add(c)
                                underscore = false
@@ -398,6 +503,10 @@ abstract class Text
                                underscore = false
                        end
                end
+               if underscore then
+                       res.append('_'.ascii.to_s)
+                       res.add('d')
+               end
                return res.to_s
        end
 
@@ -450,6 +559,50 @@ abstract class Text
        #     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'"
+       fun escape_to_sh: String do
+               var b = new FlatBuffer
+               b.chars.add '\''
+               for i in [0..length[ do
+                       var c = chars[i]
+                       if c == '\'' then
+                               b.append("'\\''")
+                       else
+                               assert without_null_byte: c != '\0'
+                               b.add(c)
+                       end
+               end
+               b.chars.add '\''
+               return b.to_s
+       end
+
+       # Escape to include in a Makefile
+       #
+       # Unfortunately, some characters are not escapable in Makefile.
+       # These characters are `;`, `|`, `\`, and the non-printable ones.
+       # They will be rendered as `"?{hex}"`.
+       fun escape_to_mk: String do
+               var b = new FlatBuffer
+               for i in [0..length[ do
+                       var c = chars[i]
+                       if c == '$' then
+                               b.append("$$")
+                       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
+                               b.add(c)
+                       end
+               end
+               return b.to_s
+       end
+
        # Return a string where Nit escape sequences are transformed.
        #
        #     var s = "\\n"
@@ -553,9 +706,11 @@ abstract class Text
                return buf.to_s
        end
 
-       # Escape the four characters `<`, `>`, `&`, and `"` with their html counterpart
+       # Escape the characters `<`, `>`, `&`, `"`, `'` and `/` as HTML/XML entity references.
+       #
+       #     assert "a&b-<>\"x\"/'".html_escape      ==  "a&amp;b-&lt;&gt;&#34;x&#34;&#47;&#39;"
        #
-       #     assert "a&b->\"x\"".html_escape      ==  "a&amp;b-&gt;&quot;x&quot;"
+       # SEE: <https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content>
        fun html_escape: SELFTYPE
        do
                var buf = new FlatBuffer
@@ -569,7 +724,11 @@ abstract class Text
                        else if c == '>' then
                                buf.append "&gt;"
                        else if c == '"' then
-                               buf.append "&quot;"
+                               buf.append "&#34;"
+                       else if c == '\'' then
+                               buf.append "&#39;"
+                       else if c == '/' then
+                               buf.append "&#47;"
                        else buf.add c
                end
 
@@ -670,8 +829,6 @@ abstract class FlatText
 
        redef var length: Int = 0
 
-       init do end
-
        redef fun output
        do
                var i = 0
@@ -691,12 +848,7 @@ private abstract class StringCharView
 
        type SELFTYPE: Text
 
-       private var target: SELFTYPE
-
-       private init(tgt: SELFTYPE)
-       do
-               target = tgt
-       end
+       var target: SELFTYPE
 
        redef fun is_empty do return target.is_empty
 
@@ -717,6 +869,11 @@ private abstract class BufferCharView
 
 end
 
+# A `String` holds and manipulates an arbitrary sequence of characters.
+#
+# String objects may be created using literals.
+#
+#     assert "Hello World!" isa String
 abstract class String
        super Text
 
@@ -737,8 +894,13 @@ abstract class String
        #     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: Iterator[String] is abstract
+
        # Returns a reversed version of self
        #
        #     assert "hello".reversed  == "olleh"
@@ -838,9 +1000,9 @@ abstract class String
        #
        # 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"
+       #     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
                if length == 0 then return self
 
@@ -869,8 +1031,6 @@ private class FlatSubstringsIter
 
        var tgt: nullable FlatText
 
-       init(tgt: FlatText) do self.tgt = tgt
-
        redef fun item do
                assert is_ok
                return tgt.as(not null)
@@ -909,7 +1069,7 @@ class FlatString
 
        redef fun reversed
        do
-               var native = calloc_string(self.length + 1)
+               var native = new NativeString(self.length + 1)
                var length = self.length
                var items = self.items
                var pos = 0
@@ -947,7 +1107,7 @@ class FlatString
 
        redef fun to_upper
        do
-               var outstr = calloc_string(self.length + 1)
+               var outstr = new NativeString(self.length + 1)
                var out_index = 0
 
                var myitems = self.items
@@ -967,7 +1127,7 @@ class FlatString
 
        redef fun to_lower
        do
-               var outstr = calloc_string(self.length + 1)
+               var outstr = new NativeString(self.length + 1)
                var out_index = 0
 
                var myitems = self.items
@@ -1012,7 +1172,7 @@ class FlatString
                if real_items != null then
                        return real_items.as(not null)
                else
-                       var newItems = calloc_string(length + 1)
+                       var newItems = new NativeString(length + 1)
                        self.items.copy_to(newItems, length, index_from, 0)
                        newItems[length] = '\0'
                        self.real_items = newItems
@@ -1090,7 +1250,7 @@ class FlatString
 
                var total_length = my_length + its_length
 
-               var target_string = calloc_string(my_length + its_length + 1)
+               var target_string = new NativeString(my_length + its_length + 1)
 
                self.items.copy_to(target_string, my_length, index_from, 0)
                if s isa FlatString then
@@ -1121,7 +1281,7 @@ class FlatString
 
                var my_items = self.items
 
-               var target_string = calloc_string((final_length) + 1)
+               var target_string = new NativeString(final_length + 1)
 
                target_string[final_length] = '\0'
 
@@ -1231,6 +1391,7 @@ private class FlatStringCharView
 
 end
 
+# A mutable sequence of characters.
 abstract class Buffer
        super Text
 
@@ -1316,20 +1477,20 @@ abstract class Buffer
        #
        # 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"
+       #     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
                if length == 0 then return
                var c = self[0].to_upper
                self[0] = c
-               var prev: Char = c
+               var prev = c
                for i in [1 .. length[ do
                        prev = c
                        c = self[i]
@@ -1419,7 +1580,7 @@ class FlatBuffer
                # The COW flag can be set at false here, since
                # it does a copy of the current `Buffer`
                written = false
-               var a = calloc_string(c+1)
+               var a = new NativeString(c+1)
                if length > 0 then items.copy_to(a, length, 0, 0)
                items = a
                capacity = c
@@ -1435,7 +1596,7 @@ class FlatBuffer
        redef fun to_cstring
        do
                if is_dirty then
-                       var new_native = calloc_string(length + 1)
+                       var new_native = new NativeString(length + 1)
                        new_native[length] = '\0'
                        if length > 0 then items.copy_to(new_native, length, 0, 0)
                        real_items = new_native
@@ -1447,11 +1608,12 @@ class FlatBuffer
        # Create a new empty string.
        init do end
 
+       # Create a new string copied from `s`.
        init from(s: Text)
        do
                capacity = s.length + 1
                length = s.length
-               items = calloc_string(capacity)
+               items = new NativeString(capacity)
                if s isa FlatString then
                        s.items.copy_to(items, length, s.index_from, 0)
                else if s isa FlatBuffer then
@@ -1471,7 +1633,7 @@ class FlatBuffer
        do
                assert cap >= 0
                # _items = new NativeString.calloc(cap)
-               items = calloc_string(cap+1)
+               items = new NativeString(cap+1)
                capacity = cap
                length = 0
        end
@@ -1528,7 +1690,7 @@ class FlatBuffer
        redef fun reverse
        do
                written = false
-               var ns = calloc_string(capacity)
+               var ns = new NativeString(capacity)
                var si = length - 1
                var ni = 0
                var it = items
@@ -1599,7 +1761,6 @@ end
 
 private class FlatBufferCharView
        super BufferCharView
-       super StringCapable
 
        redef type SELFTYPE: FlatBuffer
 
@@ -1632,7 +1793,6 @@ private class FlatBufferCharView
 
        redef fun append(s)
        do
-               var my_items = target.items
                var s_length = s.length
                if target.capacity < s.length then enlarge(s_length + target.length)
        end
@@ -1682,7 +1842,7 @@ redef class Object
 
        # 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`.
@@ -1799,13 +1959,15 @@ redef class Float
                return str
        end
 
-       # `self` representation with `nb` digits after the '.'.
+       # `String` representation of `self` with the given number of `decimals`
        #
-       #     assert 12.345.to_precision(1) == "12.3"
-       #     assert 12.345.to_precision(2) == "12.35"
-       #     assert 12.345.to_precision(3) == "12.345"
-       #     assert 12.345.to_precision(4) == "12.3450"
-       fun to_precision(nb: Int): String
+       #     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
                if is_nan then return "nan"
 
@@ -1816,25 +1978,34 @@ redef class Float
                        return  "-inf"
                end
 
-               if nb == 0 then return self.to_i.to_s
+               if decimals == 0 then return self.to_i.to_s
                var f = self
-               for i in [0..nb[ do f = f * 10.0
+               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"
-               var s = i.to_s
+               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
-               if sl > nb then
-                       var p1 = s.substring(0, s.length-nb)
-                       var p2 = s.substring(s.length-nb, nb)
-                       return p1 + "." + p2
+               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
-                       return "0." + ("0"*(nb-sl)) + s
+                       p1 = "0"
+                       p2 = "0"*(decimals-sl) + s
                end
+
+               if i < 0 then p1 = "-" + p1
+
+               return p1 + "." + p2
        end
 
        # `self` representation with `nb` digits after the '.'.
@@ -1866,10 +2037,10 @@ 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
        fun is_numeric: Bool
        do
                return self >= '0' and self <= '9'
@@ -1877,10 +2048,10 @@ 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
        fun is_alpha: Bool
        do
                return (self >= 'a' and self <= 'z') or (self >= 'A' and self <= 'Z')
@@ -1888,11 +2059,11 @@ redef class Char
 
        # 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
        fun is_alphanumeric: Bool
        do
                return self.is_numeric or self.is_alpha
@@ -2024,11 +2195,16 @@ end
 
 # Native strings are simple C char *
 extern class NativeString `{ char* `}
-       super StringCapable
        # Creates a new NativeString with a capacity of `length`
        new(length: Int) is intern
+
+       # Get char at `index`.
        fun [](index: Int): Char is intern
+
+       # Set char `item` at index.
        fun []=(index: Int, item: Char) is intern
+
+       # Copy `self` to `dest`.
        fun copy_to(dest: NativeString, length: Int, from: Int, to: Int) is intern
 
        # Position of the first nul character.
@@ -2038,7 +2214,11 @@ extern class NativeString `{ char* `}
                while self[l] != '\0' do l += 1
                return l
        end
+
+       # Parse `self` as an Int.
        fun atoi: Int is intern
+
+       # Parse `self` as a Float.
        fun atof: Float is extern "atof"
 
        redef fun to_s
@@ -2046,30 +2226,27 @@ extern class NativeString `{ char* `}
                return to_s_with_length(cstring_length)
        end
 
+       # Returns `self` as a String of `length`.
        fun to_s_with_length(length: Int): FlatString
        do
                assert length >= 0
                var str = new FlatString.with_infos(self, length, 0, length - 1)
-               str.real_items = self
                return str
        end
 
+       # Returns `self` as a new String.
        fun to_s_with_copy: FlatString
        do
                var length = cstring_length
-               var new_self = calloc_string(length + 1)
+               var new_self = new NativeString(length + 1)
                copy_to(new_self, length, 0, 0)
                var str = new FlatString.with_infos(new_self, length, 0, length - 1)
-               str.real_items = self
+               new_self[length] = '\0'
+               str.real_items = new_self
                return str
        end
 end
 
-# StringCapable objects can create native strings
-interface StringCapable
-       protected fun calloc_string(size: Int): NativeString is intern
-end
-
 redef class Sys
        private var args_cache: nullable Sequence[String]