1# strings - operations on text.2#3# A string is a run of bytes and everything here counts bytes, the way Go does4# it: Index gives a byte offset, and slicing at one that falls inside a letter5# outside ASCII cuts the letter in half. The utf8 module is where the code6# points are.7#8# strings = import("strings")9#10# strings.Split("a,b,c", ",") # [a, b, c]11# strings.Cut("key=value", "=") # {Before: key, After: value, Found: true}12# strings.TrimSpace(" hi\n") # hi13#14# Nothing is modified in place, since a string cannot be: every function here15# returns a new one.1617buffer = import("buffer")1819spaces = "\t\n\v\f\r "2021Contains = fn(str, sub) {22 maxAttempts = len(str) - len(sub)23 for i = 0; i <= maxAttempts; ++i {24 if sub == slice(str, i, i + len(sub)) {25 return true26 }27 }2829 return false30}3132ContainsAny = fn(str, chars) {33 for i = 0; i < len(chars); ++i {34 if Contains(str, chars[i]) {35 return true36 }37 }3839 return false40}4142Count = fn(str, sub) {43 total = 04445 maxAttempts = len(str) - len(sub)46 for i = 0; i <= maxAttempts; ++i {47 if sub == slice(str, i, i + len(sub)) {48 ++total49 }50 }5152 return total53}5455cutResult = fn(before, after, found) {56 res = new()57 res.Before = before58 res.After = after59 res.Found = found6061 return res62}6364Cut = fn(str, sub) {65 maxAttempts = len(str) - len(sub)66 for i = 0; i <= maxAttempts; ++i {67 if sub == slice(str, i, i + len(sub)) {68 return cutResult(69 slice(str, 0, i),70 slice(str, i + len(sub), len(str)),71 true72 )73 }74 }7576 return cutResult(str, "", false)77}7879HasPrefix = fn(str, pre) {80 if len(pre) > len(str) {81 return false82 }8384 return slice(str, 0, len(pre)) == pre85}8687HasSuffix = fn(str, sub) {88 if len(sub) > len(str) {89 return false90 }9192 return slice(str, len(str) - len(sub), len(str)) == sub93}9495Index = fn(str, sub) {96 maxAttempts = len(str) - len(sub)97 for i = 0; i <= maxAttempts; ++i {98 if sub == slice(str, i, i + len(sub)) {99 return i100 }101 }102103 return -1104}105106IndexAny = fn(str, chars) {107 index = -1108 for i = 0; i < len(chars); ++i {109 tmp = Index(str, chars[i])110 if tmp != -1 && index > tmp || index == -1 {111 index = tmp112 }113 }114115 return index116}117118Join = fn(arr, sep) {119 b = buffer.Builder()120121 for i = 0; i < len(arr); ++i {122 if type(arr[i]) != "string" {123 return error("array element at index {i} is not a string")124 }125126 if i > 0 {127 b.Write(sep)128 }129 b.Write(arr[i])130 }131132 return b.String()133}134135LastIndex = fn(str, sub) {136 for i = len(str) - len(sub); i >= 0; --i {137 if sub == slice(str, i, i + len(sub)) {138 return i139 }140 }141142 return -1143}144145LastIndexAny = fn(str, chars) {146 index = -1147 for i = 0; i < len(chars); ++i {148 tmp = LastIndex(str, chars[i])149 if tmp > index {150 index = tmp151 }152 }153154 return index155}156157Repeat = fn(str, n) {158 b = buffer.Builder()159160 for i = 0; i < n; ++i {161 b.Write(str)162 }163164 return b.String()165}166167Reverse = fn(str) {168 b = buffer.Builder()169170 for i = len(str) - 1; i >= 0; --i {171 b.Write(str[i])172 }173174 return b.String()175}176177SplitAfterN = fn(str, sep, n) {178 if sep == "" {179 return error("splitAfter: empty separator")180 }181182 ret = []183 for (idx = Index(str, sep)) != -1 && n != 0 {184 ret = append(ret, slice(str, 0, idx + len(sep)))185 str = slice(str, idx + len(sep), len(str))186 --n187 }188189 return append(ret, str)190}191192SplitAfter = fn(str, sep) { SplitAfterN(str, sep, -1) }193194SplitN = fn(str, sep, n) {195 if sep == "" {196 return error("split: empty separator")197 }198199 # Empty pieces are kept, "a,,b" splits into three.200 ret = []201 for (idx = Index(str, sep)) != -1 && n != 0 {202 ret = append(ret, slice(str, 0, idx))203 str = slice(str, idx + len(sep), len(str))204 --n205 }206207 return append(ret, str)208}209210Split = fn(str, sep) { SplitN(str, sep, -1) }211212# Fields splits around runs of whitespace, dropping the empty pieces.213Fields = fn(str) {214 ret = []215 field = ""216217 for i = 0; i < len(str); ++i {218 if Contains(spaces, str[i]) {219 if len(field) > 0 {220 ret = append(ret, field)221 field = ""222 }223 } else {224 field = field + str[i]225 }226 }227228 if len(field) > 0 {229 ret = append(ret, field)230 }231 return ret232}233234TrimPrefix = fn(str, pre) {235 return if len(pre) > len(str) || pre != slice(str, 0, len(pre)) {236 str237 } else {238 slice(str, len(pre), len(str))239 }240}241242TrimSuffix = fn(str, sub) {243 return if len(sub) > len(str) || sub != slice(str, len(str) - len(sub), len(str)) {244 str245 } else {246 slice(str, 0, len(str) - len(sub))247 }248}249250ReplaceAll = fn(str, old, new) { Join(Split(str, old), new) }251252Replace = fn(str, old, new, n) {253 if n < 0 {254 return ReplaceAll(str, old, new)255 }256257 ret = ""258 for i = 0; i < n; ++i {259 if (idx = Index(str, old)) == -1 {260 break261 }262 ret += slice(str, 0, idx) + new263 str = slice(str, idx + len(old), len(str))264 }265266 return ret + str267}268269isalpha = fn(char) { char >= 97 && char <= 122 || char >= 65 && char <= 90 }270islower = fn(char) { char >= 97 && char <= 122 }271isupper = fn(char) { char >= 65 && char <= 90 }272toupper = fn(char) { char - 32 }273tolower = fn(char) { char + 32 }274275ToUpper = fn(str) {276 b = bytes(str)277 ret = []278279 for i = 0; i < len(b); ++i {280 char = b[i]281 ret = append(ret, if islower(char) { toupper(char) } else { char })282 }283284 return string(bytes(ret))285}286287ToLower = fn(str) {288 b = bytes(str)289 ret = []290291 for i = 0; i < len(b); ++i {292 char = b[i]293 ret = append(ret, if isupper(char) { tolower(char) } else { char })294 }295296 return string(bytes(ret))297}298299ToTitle = fn(str) {300 toks = Split(str, " ")301 ret = []302303 for i = 0; i < len(toks); ++i {304 b = bytes(toks[i])305 tmp = []306307 tmp = append(tmp, if islower(b[0]) { toupper(b[0]) } else { b[0] })308 for j = 1; j < len(b); ++j {309 tmp = append(tmp, b[j])310 }311312 ret = append(ret, string(bytes(tmp)))313 }314315 return Join(ret, " ")316}317318TrimLeft = fn(str, cutset) {319 for start = 0; start < len(str); ++start {320 if !Contains(cutset, str[start]) {321 break322 }323 }324325 return slice(str, start, len(str))326}327328TrimRight = fn(str, cutset) {329 for stop = len(str); stop > 0; --stop {330 if !Contains(cutset, str[stop - 1]) {331 break332 }333 }334335 return slice(str, 0, stop)336}337338# PadLeft returns str padded with pad until it is width long, or str itself339# if it is already that long. Pad defaults to a space.340PadLeft = fn(str, width, pad) {341 if pad == null || len(pad) == 0 {342 pad = " "343 }344345 b = buffer.Builder()346 for len(str) + b.Len() < width {347 b.Write(pad)348 }349 b.Write(str)350351 return b.String()352}353354# PadRight is PadLeft, with the padding on the other side.355PadRight = fn(str, width, pad) {356 if pad == null || len(pad) == 0 {357 pad = " "358 }359360 b = buffer.Builder()361 b.Write(str)362 for b.Len() < width {363 b.Write(pad)364 }365366 return b.String()367}368369Trim = fn(str, cutset) { TrimRight(TrimLeft(str, cutset), cutset) }370371TrimSpace = fn(str) { Trim(str, spaces) }