| 1 | use System.Text.RegularExpressions |
|---|
| 2 | |
|---|
| 3 | |
|---|
| 4 | namespace Cobra.Lang |
|---|
| 5 | |
|---|
| 6 | extend System.Object |
|---|
| 7 | |
|---|
| 8 | def typeOf as System.Type |
|---|
| 9 | """ |
|---|
| 10 | Return the Type of the object this is called on. |
|---|
| 11 | Using .typeOf is portable between platforms in contrast to CLR .getType and JVM .getClass. |
|---|
| 12 | """ |
|---|
| 13 | return .getType |
|---|
| 14 | |
|---|
| 15 | def toTechString as String |
|---|
| 16 | """ |
|---|
| 17 | Generate a 'technical' string representation of the object suitable for inspection and debugging. |
|---|
| 18 | Used for AssertExceptions (and subclasses such as RequireException) and the `trace` statement. |
|---|
| 19 | It provides more information about collections and enumerations and recovers from exceptions |
|---|
| 20 | when making strings so that debugging can proceed. |
|---|
| 21 | """ |
|---|
| 22 | return CobraImp._techStringMaker.makeString(this) |
|---|
| 23 | |
|---|
| 24 | def toPrintString as String |
|---|
| 25 | """ |
|---|
| 26 | Generate a string representation of the object suitable for printing. |
|---|
| 27 | Strings are only quoted if they are inside collections such as a list or dictionary. |
|---|
| 28 | """ |
|---|
| 29 | return CobraImp._printStringMaker.makeString(this) |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | extend String |
|---|
| 33 | |
|---|
| 34 | def before(sep as String) as String |
|---|
| 35 | test |
|---|
| 36 | s = 'abc|de' |
|---|
| 37 | assert s.before('|') == 'abc' |
|---|
| 38 | assert s.before('=') == 'abc|de' |
|---|
| 39 | assert '|abcde'.before('|') == '' |
|---|
| 40 | assert 'abcde|'.before('|') == 'abcde' |
|---|
| 41 | body |
|---|
| 42 | index = .indexOf(sep) |
|---|
| 43 | if index < 0, return this |
|---|
| 44 | return .substring(0, index) |
|---|
| 45 | |
|---|
| 46 | def after(sep as String) as String |
|---|
| 47 | test |
|---|
| 48 | s = 'abc|de' |
|---|
| 49 | assert s.after('|') == 'de' |
|---|
| 50 | assert s.after('=') == '' |
|---|
| 51 | assert '|abcde'.after('|') == 'abcde' |
|---|
| 52 | assert 'abcde|'.after('|') == '' |
|---|
| 53 | body |
|---|
| 54 | index = .indexOf(sep) |
|---|
| 55 | if index < 0, return '' |
|---|
| 56 | return .substring(index+sep.length) |
|---|
| 57 | |
|---|
| 58 | def capitalized as String |
|---|
| 59 | """ |
|---|
| 60 | Returns the string with the first character capitalized. |
|---|
| 61 | Returns a blank string for a blank string. |
|---|
| 62 | """ |
|---|
| 63 | ensure |
|---|
| 64 | result.length == .length |
|---|
| 65 | result.length implies result[0] == result[0].toUpper |
|---|
| 66 | test |
|---|
| 67 | assert 'chuck'.capitalized == 'Chuck' |
|---|
| 68 | assert 'Chuck'.capitalized == 'Chuck' |
|---|
| 69 | assert ''.capitalized == '' |
|---|
| 70 | assert ' foo'.capitalized == ' foo' |
|---|
| 71 | assert 'f'.capitalized == 'F' |
|---|
| 72 | assert '1aoeu'.capitalized == '1aoeu' |
|---|
| 73 | body |
|---|
| 74 | if .length == 0, return this |
|---|
| 75 | return this[:1].toUpper + this[1:] |
|---|
| 76 | |
|---|
| 77 | def count(c as char) as int |
|---|
| 78 | """ Return a count of the number of occurrences of the char in this string. """ |
|---|
| 79 | test |
|---|
| 80 | assert ''.count(c'x')==0 |
|---|
| 81 | assert 'x'.count(c'x')==1 |
|---|
| 82 | assert 'X'.count(c'x')==0 # case sensitive |
|---|
| 83 | assert ' ! ! '.count(c'!')==2 |
|---|
| 84 | body |
|---|
| 85 | count = 0 |
|---|
| 86 | for ch in this, if c == ch, count += 1 |
|---|
| 87 | return count |
|---|
| 88 | |
|---|
| 89 | def count(substring as String) as int |
|---|
| 90 | """ Return a count of the number of occurrences of the substring in this string. """ |
|---|
| 91 | test |
|---|
| 92 | assert ''.count('aoeu') == 0 |
|---|
| 93 | assert 'x'.count('x') == 1 |
|---|
| 94 | assert 'xyxy'.count('xy') == 2 |
|---|
| 95 | assert 'xyxy'.count('a') == 0 |
|---|
| 96 | body |
|---|
| 97 | return (.length - .replace(substring, '').length) // substring.length |
|---|
| 98 | |
|---|
| 99 | def isCapitalized as bool |
|---|
| 100 | """ Return a bool indicating if this string is nonzero length and starts with an uppercase letter. """ |
|---|
| 101 | test |
|---|
| 102 | assert 'Aoeu'.isCapitalized |
|---|
| 103 | assert 'Zaoeu'.isCapitalized |
|---|
| 104 | assert not 'aoeu'.isCapitalized |
|---|
| 105 | assert not ''.isCapitalized |
|---|
| 106 | assert not '1234'.isCapitalized |
|---|
| 107 | body |
|---|
| 108 | return .length and this[0].isUpper |
|---|
| 109 | |
|---|
| 110 | def md5HashInHex as String |
|---|
| 111 | """ Return a string of Hex characters of the md5 hash of this string. """ |
|---|
| 112 | ensure |
|---|
| 113 | result.length == 32 |
|---|
| 114 | test |
|---|
| 115 | assert 'Black holes and revelations.'.md5HashInHex == '95b141d670c19f2f20a820751897b9c6' |
|---|
| 116 | body |
|---|
| 117 | md5 = System.Security.Cryptography.MD5CryptoServiceProvider() |
|---|
| 118 | data = System.Text.Encoding.ascii.getBytes(this) # why ASCII? why not utf8 or something? |
|---|
| 119 | data = md5.computeHash(data) |
|---|
| 120 | ret = '' |
|---|
| 121 | for i in data.length |
|---|
| 122 | ret += data[i].toString('x2') |
|---|
| 123 | return ret |
|---|
| 124 | |
|---|
| 125 | def repeat(times as int) as String |
|---|
| 126 | """ |
|---|
| 127 | Return the string repeated a number of times. |
|---|
| 128 | """ |
|---|
| 129 | test |
|---|
| 130 | assert 'xy'.repeat(3) == 'xyxyxy' |
|---|
| 131 | assert ''.repeat(1_000_000) == '' |
|---|
| 132 | body |
|---|
| 133 | len = .length |
|---|
| 134 | sb = StringBuilder(len * times) |
|---|
| 135 | if len, for i in times, sb.append(this) |
|---|
| 136 | return sb.toString |
|---|
| 137 | |
|---|
| 138 | def split(separator as String) as String[] |
|---|
| 139 | return .split(separator, 2_147_483_647, StringSplitOptions.None) # CC: int.max |
|---|
| 140 | |
|---|
| 141 | def split(separator as String, count as int) as String[] |
|---|
| 142 | require count >= 0 |
|---|
| 143 | return .split(separator, count, StringSplitOptions.None) |
|---|
| 144 | |
|---|
| 145 | def split(separator as String, options as StringSplitOptions) as String[] |
|---|
| 146 | return .split(separator, 2_147_483_647, options) # CC: int.max |
|---|
| 147 | |
|---|
| 148 | def split(separator as String, count as int, options as StringSplitOptions) as String[] |
|---|
| 149 | """ |
|---|
| 150 | Return an array of strings created by splitting this string by the given separator |
|---|
| 151 | up to a maximum of count items, conforming to the given StringSplitOptions. |
|---|
| 152 | """ |
|---|
| 153 | require |
|---|
| 154 | count >= 0 |
|---|
| 155 | test |
|---|
| 156 | big = 100 |
|---|
| 157 | assert ''.split('aoeu', big) == @[''] |
|---|
| 158 | assert 'aoeu'.split(' ', 0) == @[] |
|---|
| 159 | assert 'aoeu'.split(' ', big) == @['aoeu'] |
|---|
| 160 | assert 'aoeuXasdf'.split('X', big) == @['aoeu', 'asdf'] |
|---|
| 161 | assert 'aoeuXXasdf'.split('XX', big) == @['aoeu', 'asdf'] |
|---|
| 162 | assert 'aoeuXXasdf'.split('XX', big) == @['aoeu', 'asdf'] |
|---|
| 163 | assert 'aoeuXXasdf'.split('XX', 0) == @[] |
|---|
| 164 | # Mono 2.6.x has a bug where Split() can return an extra substring. |
|---|
| 165 | #assert 'aoeuXXasdf'.split('XX', 1) == ['aoeuXXasdf'] |
|---|
| 166 | #assert 'aoeuXXasdf'.split('XX', 2) == ['aoeu', 'asdf'] |
|---|
| 167 | #assert 'aoeuXXasdf'.split('XX', 3) == ['aoeu', 'asdf'] |
|---|
| 168 | body |
|---|
| 169 | return .split(@[separator], count, options) to ! |
|---|
| 170 | |
|---|
| 171 | def split(chars as List<of char>) as List<of String> |
|---|
| 172 | """ Split this string on any of the list of chars given returning a List of Strings. """ |
|---|
| 173 | test |
|---|
| 174 | s = 'a,b:c:d,e,f' |
|---|
| 175 | assert s.split([c',', c':']) == ['a', 'b', 'c', 'd', 'e', 'f'] |
|---|
| 176 | body |
|---|
| 177 | return List<of String>(.split(chars.toArray) to !) |
|---|
| 178 | |
|---|
| 179 | def split(chars as IList<of char>) as List<of String> |
|---|
| 180 | """ Split this string on any of the IList of chars given returning a List of Strings. """ |
|---|
| 181 | charsArray = char[](chars.count) |
|---|
| 182 | for i in chars.count, charsArray[i] = chars[i] |
|---|
| 183 | return List<of String>(.split(charsArray)) |
|---|
| 184 | |
|---|
| 185 | def splitLines as List<of String> |
|---|
| 186 | return .splitLines(false) |
|---|
| 187 | |
|---|
| 188 | def splitLines(keepEnds as bool) as List<of String> |
|---|
| 189 | """ |
|---|
| 190 | Returns the string split into lines, recognizing the various line endings (posix, dos/http, old mac) even if mixed within the same string. |
|---|
| 191 | If keepEnds is true the nl separators are left on the end of each line. |
|---|
| 192 | """ |
|---|
| 193 | test |
|---|
| 194 | cases = [ |
|---|
| 195 | ['', false, []], |
|---|
| 196 | [' ', false, [' ']], |
|---|
| 197 | ['x', false, ['x']], |
|---|
| 198 | ['x y', false, ['x y']], |
|---|
| 199 | |
|---|
| 200 | ['a\n', false, ['a']], |
|---|
| 201 | ['a\n', true, ['a\n']], |
|---|
| 202 | ['a\nb', false, ['a', 'b']], |
|---|
| 203 | ['a\nb', true, ['a\n', 'b']], |
|---|
| 204 | ['a\nb\n', false, ['a', 'b']], |
|---|
| 205 | ['a\nb\n', true, ['a\n', 'b\n']], |
|---|
| 206 | |
|---|
| 207 | ['a\r', false, ['a']], |
|---|
| 208 | ['a\r', true, ['a\r']], |
|---|
| 209 | ['a\rb', false, ['a', 'b']], |
|---|
| 210 | ['a\rb', true, ['a\r', 'b']], |
|---|
| 211 | ['a\rb\r', false, ['a', 'b']], |
|---|
| 212 | ['a\rb\r', true, ['a\r', 'b\r']], |
|---|
| 213 | |
|---|
| 214 | ['a\r\n', false, ['a']], |
|---|
| 215 | ['a\r\n', true, ['a\r\n']], |
|---|
| 216 | ['a\r\nb', false, ['a', 'b']], |
|---|
| 217 | ['a\r\nb', true, ['a\r\n', 'b']], |
|---|
| 218 | ['a\r\nb\r\n', false, ['a', 'b']], |
|---|
| 219 | ['a\r\nb\r\n', true, ['a\r\n', 'b\r\n']], |
|---|
| 220 | |
|---|
| 221 | ['a\r\n\r\n', false, ['a', '']], |
|---|
| 222 | ['a\r\n\r\n', true, ['a\r\n', '\r\n']], |
|---|
| 223 | ['a\r\n\r\n\r\n', false, ['a', '', '']], |
|---|
| 224 | ['a\r\n\r\n\r\n', true, ['a\r\n', '\r\n', '\r\n']], |
|---|
| 225 | |
|---|
| 226 | ['a\rb\nc\r\nd', false, ['a', 'b', 'c', 'd']], |
|---|
| 227 | ['a\rb\nc\r\nd', true, ['a\r', 'b\n', 'c\r\n', 'd']], |
|---|
| 228 | ] |
|---|
| 229 | for a, b, c in cases |
|---|
| 230 | input = a to String |
|---|
| 231 | keepEnds = b to bool |
|---|
| 232 | expected = c |
|---|
| 233 | # trace input, keepEnds, expected |
|---|
| 234 | actual = input.splitLines(keepEnds) |
|---|
| 235 | assert actual == expected |
|---|
| 236 | body |
|---|
| 237 | # posix = \n |
|---|
| 238 | # old mac = \r |
|---|
| 239 | # dos, http = \r\n |
|---|
| 240 | lines = List<of String>() |
|---|
| 241 | len = .length |
|---|
| 242 | i = j = 0 |
|---|
| 243 | while i < len |
|---|
| 244 | while i < len |
|---|
| 245 | c = this[i] |
|---|
| 246 | if c == c'\n' or c == c'\r', break |
|---|
| 247 | i += 1 |
|---|
| 248 | eoli = i |
|---|
| 249 | if i < len |
|---|
| 250 | if this[i] == '\r' and i+1 < len and this[i+1] == '\n' |
|---|
| 251 | i += 2 |
|---|
| 252 | else |
|---|
| 253 | i += 1 |
|---|
| 254 | if keepEnds, eoli = i |
|---|
| 255 | lines.add(this[j:eoli]) |
|---|
| 256 | j = i |
|---|
| 257 | if j < len, lines.add(this[j:]) |
|---|
| 258 | return lines |
|---|
| 259 | |
|---|
| 260 | def splitWords as List<of String> |
|---|
| 261 | """ Split this string into (non-whitespace) words returning them as a list of strings. """ |
|---|
| 262 | test |
|---|
| 263 | # preliminary: |
|---|
| 264 | assert 'foo bar'.split == ['foo', 'bar'] |
|---|
| 265 | assert 'foo bar'.split == ['foo', '', 'bar'] # undesireable |
|---|
| 266 | assert 'foo bar'.split == ['foo', '', '', 'bar'] # undesireable |
|---|
| 267 | assert Regex(r'\s+').split('foo bar\r\nbaz') == ['foo', 'bar', 'baz'] |
|---|
| 268 | # this method: |
|---|
| 269 | assert 'foo bar\r\nbaz'.splitWords == ['foo', 'bar', 'baz'] |
|---|
| 270 | body |
|---|
| 271 | # to-do: could bypass Regex for better speed |
|---|
| 272 | return Regex(r'\s+').split(this).toList |
|---|
| 273 | |
|---|
| 274 | |
|---|
| 275 | class DecimalTools |
|---|
| 276 | |
|---|
| 277 | # Cobra does not yet support extensions of primitive types |
|---|
| 278 | # But the compiler will pick up "DecimalTools" as if it were |
|---|
| 279 | |
|---|
| 280 | shared |
|---|
| 281 | |
|---|
| 282 | def pow(x as decimal, y as decimal) as decimal |
|---|
| 283 | test |
|---|
| 284 | assert DecimalTools.pow(1, 1) == 1 |
|---|
| 285 | assert DecimalTools.pow(1, 2) == 1 |
|---|
| 286 | assert DecimalTools.pow(2, 2) == 4 |
|---|
| 287 | assert DecimalTools.pow(2, 3) == 8 |
|---|
| 288 | assert DecimalTools.pow(3, 3) == 27 |
|---|
| 289 | assert DecimalTools.pow(3, 4.5d).round(3) == 140.296d |
|---|
| 290 | body |
|---|
| 291 | if y.remainder(1.0d) == 0 and y > 0 and y <= Int32.maxValue |
|---|
| 292 | # a round power like 5.0 |
|---|
| 293 | r = x |
|---|
| 294 | for i in (y to int)-1, r *= x |
|---|
| 295 | return r |
|---|
| 296 | else |
|---|
| 297 | # would be nice to have a real algorithm for this to avoid the conversion to float |
|---|
| 298 | # which has a smaller range, fewer significant digits and lossy representation of |
|---|
| 299 | # some of decimal's values |
|---|
| 300 | return (x to float).pow(y to float) to decimal |
|---|
| 301 | |
|---|
| 302 | def sqrt(x as decimal) as decimal |
|---|
| 303 | test |
|---|
| 304 | assert DecimalTools.sqrt(1) == 1 |
|---|
| 305 | assert DecimalTools.sqrt(2).round(3) == 1.414d |
|---|
| 306 | assert DecimalTools.sqrt(3).round(3) == 1.732d |
|---|
| 307 | assert DecimalTools.sqrt(4) == 2 |
|---|
| 308 | assert DecimalTools.sqrt(4.5d).round(3) == 2.121d |
|---|
| 309 | expect OverflowException, DecimalTools.sqrt(-4.5d) |
|---|
| 310 | body |
|---|
| 311 | # would be nice to have a real algorithm for this to avoid the conversion to float |
|---|
| 312 | # which has a smaller range, fewer significant digits and lossy representation of |
|---|
| 313 | # some of decimal's values |
|---|
| 314 | return (x to float).sqrt to decimal |
|---|