# If `self` contains only digits and alpha <= 'f', return the corresponding integer.
#
# assert "ff".to_hex == 255
- fun to_hex: Int do return a_to(16)
+ fun to_hex(pos, ln: nullable Int): Int do
+ var res = 0
+ if pos == null then pos = 0
+ if ln == null then ln = length - pos
+ var max = pos + ln
+ for i in [pos .. max[ do
+ res <<= 4
+ res += self[i].from_hex
+ end
+ return res
+ end
# If `self` contains only digits <= '7', return the corresponding integer.
#
end
end
- # Returns `true` if the string contains only Numeric values (and one "," or one "." character)
+ # Is this string in a valid numeric format compatible with `to_f`?
#
# assert "123".is_numeric == true
# assert "1.2".is_numeric == true
- # assert "1,2".is_numeric == true
+ # assert "-1.2".is_numeric == true
+ # assert "-1.23e-2".is_numeric == true
# assert "1..2".is_numeric == false
+ # assert "".is_numeric == false
fun is_numeric: Bool
do
- var has_point_or_comma = false
+ var has_point = false
+ var e_index = -1
for i in [0..length[ do
var c = chars[i]
if not c.is_numeric then
- if (c == '.' or c == ',') and not has_point_or_comma then
- has_point_or_comma = true
+ if c == '.' and not has_point then
+ has_point = true
+ else if c == 'e' and e_index == -1 and i > 0 and i < length - 1 and chars[i-1] != '-' then
+ e_index = i
+ else if c == '-' and i == e_index + 1 and i < length - 1 then
else
return false
end
end
end
- return true
+ return not is_empty
end
# Returns `true` if the string contains only Hex chars
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.5, '.') == "..hello..."
#
# If `length` is not enough, `self` is returned as is.
#
# 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.
b.append("\\\\")
else if c.code_point < 32 then
b.add('\\')
- var oct = c.code_point.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
b.add('\\')
b.add(c)
else if c.code_point < 32 or c == ';' or c == '|' or c == '\\' or c == '=' then
- b.append("?{c.code_point.to_base(16, false)}")
+ b.append("?{c.code_point.to_base(16)}")
else
b.add(c)
end
# assert "\\ud800\\udfd3".from_utf16_escape == '๐'
# assert "\\u00e8".from_utf16_escape == 'รจ'
# assert "\\u3042".from_utf16_escape == 'ใ'
- fun from_utf16_escape: Char do
- var ln = length
- if ln != 6 and ln != 12 then return 0xFFFD.code_point
- var cphi = substring(2, 4).to_hex
- if cphi < 0xD800 then return cphi.code_point
- if cphi > 0xDFFF then return cphi.code_point
- if cphi > 0xDBFF then return 0xFFFD.code_point
- var cp = 0
- cp += (cphi - 0xD800) << 10
- var cplo = substring(8, 4).to_hex
+ fun from_utf16_escape(pos, ln: nullable Int): Char do
+ if pos == null then pos = 0
+ if ln == null then ln = length - pos
+ if ln < 6 then return 0xFFFD.code_point
+ var cp = from_utf16_digit(pos + 2)
+ if cp < 0xD800 then return cp.code_point
+ if cp > 0xDFFF then return cp.code_point
+ if cp > 0xDBFF then return 0xFFFD.code_point
+ if ln == 6 then return 0xFFFD.code_point
+ if ln < 12 then return 0xFFFD.code_point
+ cp <<= 16
+ cp += from_utf16_digit(pos + 8)
+ var cplo = cp & 0xFFFF
if cplo < 0xDC00 then return 0xFFFD.code_point
if cplo > 0xDFFF then return 0xFFFD.code_point
- cp += cplo - 0xDC00
- cp += 0x10000
- return cp.code_point
+ return cp.from_utf16_surr.code_point
+ end
+
+ # Returns a UTF-16 escape value
+ #
+ # var s = "\\ud800\\udfd3"
+ # assert s.from_utf16_digit(2) == 0xD800
+ # assert s.from_utf16_digit(8) == 0xDFD3
+ fun from_utf16_digit(pos: nullable Int): Int do
+ if pos == null then pos = 0
+ return to_hex(pos, 4)
end
# Encode `self` to percent (or URL) encoding
l += 1
end
- return buf.to_s_with_length(l)
+ return buf.to_s_unsafe(l)
end
# Escape the characters `<`, `>`, `&`, `"`, `'` and `/` as HTML/XML entity references.
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`
+ #
+ # 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 %1 is a %2.".format("String", "formatted String") == "This String is a formatted String."
- # assert "\\%1 This string".format("String") == "\\%1 This string"
+ # 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
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
#
# var ns = new NativeString(8)
# "Text is String".copy_to_native(ns, 8, 2, 0)
- # assert ns.to_s_with_length(8) == "xt is St"
+ # assert ns.to_s_unsafe(8) == "xt is St"
#
fun copy_to_native(dest: NativeString, n, src_offset, dest_offset: Int) do
var mypos = src_offset
var ns = new NativeString(nslen + 1)
ns[nslen] = 0u8
native_byte_to_s(ns, nslen + 1)
- return ns.to_s_with_length(nslen)
+ return ns.to_s_unsafe(nslen)
end
end
redef class Int
# Wrapper of strerror C function
- private fun strerror_ext: NativeString `{ return strerror(self); `}
+ private fun strerror_ext: NativeString `{ return strerror((int)self); `}
# Returns a string describing error number
fun strerror: String do return strerror_ext.to_s
- # 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
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)
+ fun to_hex: String do return to_base(16)
end
redef class Float
# assert 'ใพ'.bytes == [0xE3u8, 0x81u8, 0xBEu8]
fun bytes: SequenceRead[Byte] do return to_s.bytes
+ # Is `self` an UTF-16 surrogate pair ?
+ fun is_surrogate: Bool do
+ var cp = code_point
+ return cp >= 0xD800 and cp <= 0xDFFF
+ end
+
# Length of `self` in a UTF-8 String
private fun u8char_len: Int do
var c = self.code_point
var ln = u8char_len
var ns = new NativeString(ln + 1)
u8char_tos(ns, ln)
- return ns.to_s_with_length(ln)
+ return ns.to_s_unsafe(ln)
end
# Returns `self` escaped to UTF-16
# 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
+ #
+ # 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 ""
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
# Returns `self` as a String of `length`.
fun to_s_with_length(length: Int): String is abstract
+ # Returns a new instance of `String` with self as `_items`
+ #
+ # /!\: Does not clean the items for compliance with UTF-8,
+ # Use only if you know what you are doing
+ fun to_s_unsafe(len: nullable Int): String is abstract
+
# Returns `self` as a String with `bytelen` and `length` set
#
# SEE: `abstract_text::Text` for more infos on the difference