-- FILE:     STRING.E
-- PURPOSE:  String (or sequence) manipulation routines for Euphoria 3.1.1.
-- AUTHOR:   Shian Lee
-- VERSION:  1.03  October/21/2019
-- LANGUAGE: Euphoria version 3.1.1 (http://www.RapidEuphoria.com)
-- PLATFORM: Any
-- LICENCE:  Free. Use at your own risk.
-- NOTE:     * See the attached reference manual STRING.DOC for details.
--           * Created using QE v2.3.9 for DOS (RapidEuphoria.com archive).
-- HISTORY:  v1.00 - initial version (January/31/2017).
--           v1.01 - using integers instead of atoms for string index (faster).
--           v1.02 - new: break(). translate() optimized for speed.
--	     v1.03 - new: rbreak().
-----------------------------------------------------------------------------



--bookmark: include Euphoria 3.1.1 library files;

include get.e       -- value(),  for val() function
include misc.e      -- sprint(), for str() function



--bookmark: Local Constants;

-- PURPOSE: commonly used constants for string/sequence manipulation
constant
    SPACE = 32,                             -- ASCII-32, Space
    EMPTY = {}                              -- empty string/sequence



--bookmark: String;

-- PURPOSE: commonly used delimiters
global constant
    WSPACE = SPACE & "\t\n\r"               -- white space (ASCII 32,9,10,13)



-- PURPOSE: return a string of spaces
-- RETURN:  string;
-- EXAMPLE: s = spc(5)  -- s is "     "
global function spc(integer len)
    return repeat(SPACE, len)               -- (ASCII 32)
end function



-- PURPOSE: return a sequence of a repeating character or string
-- RETURN:  sequence;
-- EXAMPLE: s = chr('*', 5)    -- s is "*****"
--          s = chr("<*>", 5)  -- s is "<*><*><*><*><*>"
global function chr(object x, integer len)
    if atom(x) then                         -- repeat 'char'
	return repeat(x, len)
    elsif length(x) = 1 then
	return repeat(x[1], len)
    elsif len then                          -- repeat "string" fast
	len *= length(x)
	while length(x) < floor(len / 2) do
	    x &= x
	end while
	return x & x[1..len - length(x)]
    else
	return EMPTY
    end if
end function



-- PURPOSE: left justify a string in a field
-- RETURN:  sequence;
-- EXAMPLE: s = lset("RapidEuphoria", 17, '*')  -- s is "RapidEuphoria****"
global function lset(sequence s, integer len, object fill)
    if len > length(s) then
	return s & repeat(fill, len - length(s))
    else
	return s[1..len]
    end if
end function



-- PURPOSE: right justify a string in a field
-- RETURN:  sequence;
-- EXAMPLE: s = rset("RapidEuphoria", 17, '*')  -- s is "****RapidEuphoria"
global function rset(sequence s, integer len, object fill)
    if len > length(s) then
	return repeat(fill, len - length(s)) & s
    else
	return s[$ - len + 1..$]
    end if
end function



-- PURPOSE: middle justify a string in a field
-- RETURN:  sequence;
-- EXAMPLE: s = mset("RapidEuphoria", 17, '*')  -- s is "**RapidEuphoria**"
global function mset(sequence s, integer len, object fill)
    if len >= length(s) then
	s = repeat(fill, floor(len - length(s)) / 2) & s &
	    repeat(fill, floor(len - length(s)) / 2 + 1)
    else
	s = s[floor(length(s) / 2) - floor(len / 2) + 1..
	      floor(length(s) / 2) + floor(len / 2) + 1]
    end if

    return s[1..len]
end function



-- PURPOSE: enclose a string with quotation marks
-- RETURN:  sequence;
-- EXAMPLE: s = quote("C:\\Long File Name") -- s is "\"C:\\Long File Name\""
global function quote(object x)
    return '"' & x & '"'
end function



-- PURPOSE: return a string representation of a number
-- RETURN:  sequence;
-- EXAMPLE: s = str(3.141592654)        -- s is "3.141592654"
--          s = str({-1, {4.5}, #FF})   -- s is "{-1,{4.5},255}"
global function str(object number)
    return sprint(number)                   -- sprint(), misc.e
end function



-- PURPOSE: convert a string representation of a number to a number
-- RETURN:  object;
-- EXAMPLE: x = val("3.141592654")      -- x is 3.141592654
--          x = val("{-1,{4.5},#FF}")   -- x is {-1, {4.5}, 255}
global function val(sequence string)
    sequence v

    v = value(string)                       -- value(), get.e
    return v[2]
end function



--bookmark: Trimming;

-- PURPOSE: remove leading characters from a string
-- RETURN:  sequence;
-- EXAMPLE: s = ltrim(" \t\t ltrim   white  spaces \n\r", WSPACE)
--              -- s is "ltrim   white  spaces \n\r"
global function ltrim(sequence s, sequence characters)
    integer l

    l = 1
    while l <= length(s) and find(s[l], characters) do
	l += 1
    end while

    return s[l..$]
end function



-- PURPOSE: remove trailing characters from a string
-- RETURN:  sequence;
-- EXAMPLE: s = rtrim(" \t\t rtrim   white  spaces \n\r", WSPACE)
--              -- s is " \t\t rtrim   white  spaces"
global function rtrim(sequence s, sequence characters)
    integer r

    r = length(s)
    while r and find(s[r], characters) do
	r -= 1
    end while

    return s[1..r]
end function



-- PURPOSE: remove leading and trailing characters from a string
-- RETURN:  sequence;
-- EXAMPLE: s = trim(" \t\t trim   white  spaces \n\r", WSPACE)
--              -- s is "trim   white  spaces"
global function trim(sequence s, sequence characters)
    integer r, l

    r = length(s)                           -- rtrim(s)
    while r and find(s[r], characters) do
	r -= 1
    end while

    l = 1                                   -- ltrim(s)
    while l <= r and find(s[l], characters) do
	l += 1
    end while

    return s[l..r]
end function



--bookmark: Searching;

-- PURPOSE: find an object in a sequence - start searching from the
--          last element (reverse find())
-- RETURN:  integer; match location, or 0 if not found
-- EXAMPLE: i = rfind('z', "abc xyz [1.23] xyz!")  -- i is 18
global function rfind(object x, sequence s)
    integer p, last

    p = find(x, s)
    last = p
    while p do
	last = p
	p = find_from(x, s, p + 1)
    end while

    return last
end function



-- PURPOSE: find an object in a sequence - start searching from any
--          element number (reverse find_from())
-- RETURN:  integer; match location, or 0 if not found
-- EXAMPLE: i = rfind_from('x', "abc xyz [1.23] xyz!", 10)  -- i is 5
global function rfind_from(object x, sequence s, integer start)
    integer p, last

    s = s[1..start]                         -- start is 0 to the length of s
    p = find(x, s)
    last = p
    while p do
	last = p
	p = find_from(x, s, p + 1)
    end while

    return last
end function



-- PURPOSE: find a sequence as a slice of another sequence - start
--          searching from the last element (reverse match())
-- RETURN:  integer; match location, or 0 if not found
-- EXAMPLE: i = rmatch("xy", "abc xyz [1.23] xyz!")  -- i is 16
global function rmatch(sequence s1, sequence s2)
    integer p, last

    p = match(s1, s2)
    last = p
    while p do
	last = p
	p = match_from(s1, s2, p + 1)
    end while

    return last
end function



-- PURPOSE: find a sequence as a slice of another sequence - start
--          searching from any element number (reverse match_from())
-- RETURN:  integer; match location, or 0 if not found
-- EXAMPLE: i = rmatch_from("xy", "abc xyz [1.23] xyz!", 16)  -- i is 5
global function rmatch_from(sequence s1, sequence s2, integer start)
    integer p, last

    s2 = s2[1..start]                       -- start is 0 to the length of s2
    p = match(s1, s2)
    last = p
    while p do
	last = p
	p = match_from(s1, s2, p + 1)
    end while

    return last
end function



--bookmark: Substring;

-- PURPOSE: return n leftmost characters in a string
-- RETURN:  sequence;
-- EXAMPLE: s = left("RapidEuphoria", 5)  -- s is "Rapid"
global function left(sequence s, integer len)
    if len < length(s) then
	return s[1..len]
    else
	return s
    end if
end function



-- PURPOSE: return n rightmost characters in a string
-- RETURN:  sequence;
-- EXAMPLE: s = right("RapidEuphoria", 8)  -- s is "Euphoria"
global function right(sequence s, integer len)
    if len < length(s) then
	return s[$ - len + 1..$]
    else
	return s
    end if
end function



-- PURPOSE: return leftmost characters in a string before a substring
-- RETURN:  sequence;
-- EXAMPLE: s = left_of("www.RapidEuphoria.com", ".", 1)  -- s is "www"
--          s = left_of("www.RapidEuphoria.com", ".", 0)
--              -- s is "www.RapidEuphoria"
global function left_of(sequence s1, sequence s2, integer first_match)
    integer p

    if first_match then                     -- get left of first match
	p = match(s2, s1)
    else                                    -- get left of last match
	p = rmatch(s2, s1)
    end if

    if p then
	return s1[1..p - 1]                 -- return left of match
    else
	return EMPTY
    end if
end function



-- PURPOSE: return rightmost characters in a string after a substring
-- RETURN:  sequence;
-- EXAMPLE: s = right_of("www.RapidEuphoria.com", ".", 0)  -- s is "com"
--          s = right_of("www.RapidEuphoria.com", ".", 1)
--              -- s is "RapidEuphoria.com"
global function right_of(sequence s1, sequence s2, integer first_match)
    integer p

    if first_match then                     -- get right of first match
	p = match(s2, s1)
    else                                    -- get right of last match
	p = rmatch(s2, s1)
    end if

    if p then
	return s1[p + length(s2)..$]        -- return right of match
    else
	return EMPTY
    end if
end function



-- PURPOSE: return part of a string
-- RETURN:  sequence;
-- EXAMPLE: s = mid("RapidEuphoria", 2, 6)  -- s is "Eu"
global function mid(sequence s, integer len, integer start)
    if start + len - 1 <= length(s) then
	return s[start..start + len - 1]
    elsif start <= length(s) then
	return s[start..$]
    else
	return EMPTY
    end if
end function



-- PURPOSE: replace part of a string with another string
-- NOTE:    change() can actually insert, replace and delete a substring
-- RETURN:  sequence;
-- EXAMPLE: s = change("Euphoria", "Rapid", 0,  1) -- s is "RapidEuphoria"
--          s = change("Euphoria", "Rapid", 8,  1) -- s is "Rapid"
--          s = change("Euphoria", ""     , 6,  3) -- s is "Eu"
--          s = change("Euphoria", "3.1.1", 0, 10) -- s is "Euphoria 3.1.1"
global function change(sequence s, object x, integer len, integer start)
    if start + len - 1 <= length(s) then
	return s[1..start - 1] & x & s[start + len..$]
    elsif start <= length(s) + 1 then
	return s[1..start - 1] & x
    else
	return s & repeat(SPACE, start - length(s) - 1) & x
    end if
end function



-- PURPOSE: replace all occurrences of a substring with another string
-- RETURN:  sequence;
-- EXAMPLE: s = translate("RapidEuphoria", "a", "**") -- s is "R**pidEuphori**"
global function translate(sequence s, sequence word, sequence new_word)
    integer p, len_w, len_n

    p = match(word, s)
    len_w = length(word)
    len_n = length(new_word)
    while p do
	s = s[1..p - 1] & new_word & s[p + len_w..$]
	p = match_from(word, s, p + len_n)
    end while

    return s
end function



-- PURPOSE: break a string into segments using a substring
-- NOTE:    will never append the substring to the last segment
-- RETURN:  sequence;
-- EXAMPLE: s = break("RapidEuphoria", '*', 3) -- s is "Rap*idE*uph*ori*a"
global function break(sequence s, object x, integer len)
    integer p, inc

    if len > 0 then
	p = len
	inc = length(x & EMPTY) + len
	while p < length(s) do
	    s = s[1..p] & x & s[p + 1..$]
	    p += inc
	end while
    end if

    return s
end function



-- PURPOSE: break a string into segments using a substring - start from the
--          last element (reverse break())
-- NOTE:    will never prepend the substring to the first segment
-- RETURN:  sequence;
-- EXAMPLE: s = rbreak("RapidEuphoria", '*', 3) -- s is "R*api*dEu*pho*ria"
global function rbreak(sequence s, object x, integer len)
    integer p

    if len > 0 then
	p = length(s) - len
	while p > 0 do
	    s = s[1..p] & x & s[p + 1..$]
	    p -= len
	end while
    end if

    return s
end function



--bookmark: Words (substrings);

-- NOTE: squeeze(), count_words(), match_words() and split() are using exactly
--       the same code algorithm, but each function return a different data.


-- PURPOSE: remove characters from a string and straighten up words
-- RETURN:  sequence;
-- EXAMPLE: s = squeeze(" \t\t squeeze     white  spaces \n\r", WSPACE, ' ')
--              -- s is "squeeze white spaces"
global function squeeze(sequence s, sequence delimiters, object fill)
    sequence words
    integer p

    words = EMPTY
    p = 0
    for i = 1 to length(s) do
	if p and find(s[i], delimiters) then
	    words &= s[p..i - 1] & fill
	    p = 0                           -- end of word
	elsif not p and not find(s[i], delimiters) then
	    p = i                           -- start of word
	end if
    end for

    if p then
	return words & s[p..length(s)]
    elsif length(words) then
	return words[1..$ - length(fill & EMPTY)]  -- i.e. $ - length(x)
    else
	return EMPTY
    end if
end function



-- PURPOSE: return the number of words and characters in a string
-- RETURN:  sequence; {words-count, characters-count}, or {0, 0} if not found
-- EXAMPLE: s = count_words("abc [1.23] xyz!", " [].!?")  -- s is {4, 9}
global function count_words(sequence s, sequence delimiters)
    integer words, chars, p

    words = 0
    chars = 0
    p = 0
    for i = 1 to length(s) do
	if p and find(s[i], delimiters) then
	    words += 1
	    chars += i - p
	    p = 0                           -- end of word
	elsif not p and not find(s[i], delimiters) then
	    p = i                           -- start of word
	end if
    end for

    if p then
	return {words + 1, chars + (length(s) + 1 - p)}
    else
	return {words, chars}
    end if
end function



-- PURPOSE: search in string to form a sequence of words-locations
-- RETURN:  sequence; {{start-of-word, end-of-word},...}, or {} if not found
-- EXAMPLE: s = match_words("abc [1.23] xyz!", " [].!?")
--              -- s is {{1,3},{6,6},{8,9},{12,14}}
global function match_words(sequence s, sequence delimiters)
    sequence words
    integer p

    words = EMPTY
    p = 0
    for i = 1 to length(s) do
	if p and find(s[i], delimiters) then
	    words = append(words, {p, i - 1})
	    p = 0                           -- end of word
	elsif not p and not find(s[i], delimiters) then
	    p = i                           -- start of word
	end if
    end for

    if p then
	return append(words, {p, length(s)})
    else
	return words
    end if
end function



-- PURPOSE: split a string to form a sequence of words
-- RETURN:  sequence; {{word1, word2,...}}, or {} if not found
-- EXAMPLE: s = split("abc [1.23] xyz!", " [].!?")
--              -- s is {"abc", "1", "23", "xyz"}
global function split(sequence s, sequence delimiters)
    sequence words
    integer p

    words = EMPTY
    p = 0
    for i = 1 to length(s) do
	if p and find(s[i], delimiters) then
	    words = append(words, s[p..i - 1])
	    p = 0                           -- end of word
	elsif not p and not find(s[i], delimiters) then
	    p = i                           -- start of word
	end if
    end for

    if p then
	return append(words, s[p..length(s)])
    else
	return words
    end if
end function



-- PURPOSE: join a sequence of words to form a string
-- RETURN:  sequence;
-- EXAMPLE: s = join({"abc", "1", "23", "xyz"}, ", ")
--              -- s is "abc, 1, 23, xyz"
global function join(sequence words, object fill)
    sequence r

    if length(words) then
	r = EMPTY
	for i = 1 to length(words) - 1 do
	    r &= words[i] & fill
	end for
	return r & words[$]
    else
	return EMPTY
    end if
end function



-- End of file.
