1 /++ 2 Various functions that do string manipulation. 3 4 Copyright: [JR](https://github.com/zorael) 5 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 6 7 Authors: 8 [JR](https://github.com/zorael) 9 +/ 10 module kameloso..string; 11 12 private: 13 14 import dialect.defs : IRCClient; 15 import std.typecons : Flag, No, Yes; 16 17 public: 18 19 20 // stripSeparatedPrefix 21 /++ 22 Strips a prefix word from a string, optionally also stripping away some 23 non-word characters (currently ":;?! "). 24 25 This is to make a helper for stripping away bot prefixes, where such may be 26 "kameloso: ". 27 28 Example: 29 --- 30 string prefixed = "kameloso: sudo MODE +o #channel :user"; 31 string command = prefixed.stripSeparatedPrefix("kameloso"); 32 assert((command == "sudo MODE +o #channel :user"), command); 33 --- 34 35 Params: 36 line = String line prefixed with `prefix`, potentially including 37 separating characters. 38 prefix = Prefix to strip. 39 demandSep = Makes it a necessity that `line` is followed 40 by one of the prefix letters ": !?;". If it isn't, the `line` string 41 will be returned as is. 42 43 Returns: 44 The passed line with the `prefix` sliced away. 45 +/ 46 auto stripSeparatedPrefix( 47 const string line, 48 const string prefix, 49 const Flag!"demandSeparatingChars" demandSep = Yes.demandSeparatingChars) pure 50 in (prefix.length, "Tried to strip separated prefix but no prefix was given") 51 { 52 import lu.string : nom, strippedLeft; 53 import std.algorithm.comparison : among; 54 import std.meta : aliasSeqOf; 55 56 enum separatingChars = ": !?;"; // In reasonable order of likelihood 57 58 string slice = line.strippedLeft; // mutable 59 60 // the onus is on the caller that slice begins with prefix, else this will throw 61 slice.nom!(Yes.decode)(prefix); 62 63 if (demandSep) 64 { 65 // Return the whole line, a non-match, if there are no separating characters 66 // (at least one of the chars in separatingChars) 67 if (!slice.length || !slice[0].among!(aliasSeqOf!separatingChars)) return line; 68 } 69 70 while (slice.length && slice[0].among!(aliasSeqOf!separatingChars)) 71 { 72 slice = slice[1..$]; 73 } 74 75 return slice.strippedLeft(separatingChars); 76 } 77 78 /// 79 unittest 80 { 81 immutable lorem = "say: lorem ipsum".stripSeparatedPrefix("say"); 82 assert((lorem == "lorem ipsum"), lorem); 83 84 immutable notehello = "note!!!! zorael hello".stripSeparatedPrefix("note"); 85 assert((notehello == "zorael hello"), notehello); 86 87 immutable sudoquit = "sudo quit :derp".stripSeparatedPrefix("sudo"); 88 assert((sudoquit == "quit :derp"), sudoquit); 89 90 /*immutable eightball = "8ball predicate?".stripSeparatedPrefix(""); 91 assert((eightball == "8ball predicate?"), eightball);*/ 92 93 immutable isnotabot = "kamelosois a bot".stripSeparatedPrefix("kameloso"); 94 assert((isnotabot == "kamelosois a bot"), isnotabot); 95 96 immutable isabot = "kamelosois a bot" 97 .stripSeparatedPrefix("kameloso", No.demandSeparatingChars); 98 assert((isabot == "is a bot"), isabot); 99 100 immutable doubles = "kameloso is a snek" 101 .stripSeparatedPrefix("kameloso"); 102 assert((doubles == "is a snek"), doubles); 103 } 104 105 106 // replaceTokens 107 /++ 108 Apply some common text replacements. Used on part and quit reasons. 109 110 Params: 111 line = String to replace tokens in. 112 client = The current [dialect.defs.IRCClient|IRCClient]. 113 114 Returns: 115 A modified string with token occurrences replaced. 116 +/ 117 auto replaceTokens(const string line, const IRCClient client) @safe pure nothrow 118 { 119 import kameloso.constants : KamelosoInfo; 120 import std.array : replace; 121 122 return line 123 .replaceTokens 124 .replace("$nickname", client.nickname); 125 } 126 127 /// 128 unittest 129 { 130 import kameloso.constants : KamelosoInfo; 131 import std.format : format; 132 133 IRCClient client; 134 client.nickname = "harbl"; 135 136 { 137 immutable line = "asdf $nickname is kameloso version $version from $source"; 138 immutable expected = "asdf %s is kameloso version %s from %s" 139 .format(client.nickname, cast(string)KamelosoInfo.version_, 140 cast(string)KamelosoInfo.source); 141 immutable actual = line.replaceTokens(client); 142 assert((actual == expected), actual); 143 } 144 { 145 immutable line = ""; 146 immutable expected = ""; 147 immutable actual = line.replaceTokens(client); 148 assert((actual == expected), actual); 149 } 150 { 151 immutable line = "blerp"; 152 immutable expected = "blerp"; 153 immutable actual = line.replaceTokens(client); 154 assert((actual == expected), actual); 155 } 156 } 157 158 159 // replaceTokens 160 /++ 161 Apply some common text replacements. Used on part and quit reasons. 162 Overload that doesn't take an [dialect.defs.IRCClient|IRCClient] and as such can't 163 replace `$nickname`. 164 165 Params: 166 line = String to replace tokens in. 167 168 Returns: 169 A modified string with token occurrences replaced. 170 +/ 171 auto replaceTokens(const string line) @safe pure nothrow 172 { 173 import kameloso.constants : KamelosoInfo; 174 import std.array : replace; 175 176 return line 177 .replace("$version", cast(string)KamelosoInfo.version_) 178 .replace("$source", cast(string)KamelosoInfo.source); 179 } 180 181 182 // replaceRandom 183 /++ 184 Replaces `$random` and `$random(i..n)` tokens in a string with corresponding 185 random values. 186 187 If given only `$random`, a value between the passed `defaultLowerBound` inclusive 188 to `defaultUpperBound` exclusive is substituted, whereas if a range of 189 `$random(i..n)` is given, a value between `i` inclusive and `n` exclusive is 190 substituted. 191 192 On syntax errors, or if `n` is not greater than `i`, the original line is 193 silently returned. 194 195 Params: 196 line = String to replace tokens in. 197 defaultLowerBound = Default lower bound when no range given. 198 defaultUpperBound = Default upper bound when no range given. 199 200 Returns: 201 A new string with occurences of `$random` and `$random(i..n)` replaced, 202 or the original string if there were no changes made. 203 +/ 204 auto replaceRandom( 205 const string line, 206 const long defaultLowerBound = 0, 207 const long defaultUpperBound = 100) @safe 208 { 209 import std.conv : text; 210 import std.random : uniform; 211 import std.string : indexOf; 212 213 immutable randomPos = line.indexOf("$random"); 214 215 if (randomPos == -1) 216 { 217 // No $random token 218 return line; 219 } 220 221 if (line.length > randomPos) 222 { 223 immutable openParen = randomPos + "$random".length; 224 225 if (line.length == openParen) 226 { 227 immutable randomNumber = uniform(defaultLowerBound, defaultUpperBound); 228 return text(line[0..randomPos], randomNumber); 229 } 230 else if (line[openParen] == '(') 231 { 232 immutable dots = line.indexOf("..", openParen); 233 234 if (dots != -1) 235 { 236 immutable endParen = line.indexOf(')', dots); 237 238 if (endParen != -1) 239 { 240 try 241 { 242 import std.conv : to; 243 244 immutable lowerBound = line[openParen+1..dots].to!long; 245 immutable upperBound = line[dots+2..endParen].to!long; 246 immutable randomNumber = uniform(lowerBound, upperBound); 247 return text(line[0..randomPos], randomNumber, line[endParen+1..$]); 248 } 249 catch (Exception _) 250 { 251 return line; 252 } 253 } 254 } 255 } 256 else if (line[openParen] == ' ') 257 { 258 immutable randomNumber = uniform(defaultLowerBound, defaultUpperBound); 259 return text(line[0..randomPos], randomNumber, line[openParen..$]); 260 } 261 } 262 263 return line; 264 } 265 266 /// 267 unittest 268 { 269 import lu.string : nom; 270 import std.conv : to; 271 272 { 273 enum line = "$random bottles of beer on the wall"; 274 string replaced = line.replaceRandom(); // mutable 275 immutable number = replaced.nom(' ').to!int; 276 assert(((number >= 0) && (number < 100)), number.to!string); 277 } 278 { 279 enum line = "$random(100..200) bottles of beer on the wall"; 280 string replaced = line.replaceRandom(); // mutable 281 immutable number = replaced.nom(' ').to!int; 282 assert(((number >= 100) && (number < 200)), number.to!string); 283 } 284 { 285 enum line = "$random(-20..-10) bottles of beer on the wall"; 286 string replaced = line.replaceRandom(); // mutable 287 immutable number = replaced.nom(' ').to!int; 288 assert(((number >= -20) && (number < -10)), number.to!string); 289 } 290 /*{ 291 static if (__VERSION__ > 2089L) 292 { 293 // Fails pre-2.090 with Error: signed integer overflow 294 enum line = "$random(-9223372036854775808..9223372036854775807) bottles of beer on the wall"; 295 string replaced = line.replaceRandom(); // mutable 296 immutable number = replaced.nom(' ').to!long; 297 //assert(((number >= cast(long)-9223372036854775808) && (number < 9223372036854775807)), number.to!string); 298 } 299 }*/ 300 { 301 // syntax error, no bounds given 302 enum line = "$random() bottles of beer on the wall"; 303 immutable replaced = line.replaceRandom(); 304 assert((replaced == line), replaced); 305 } 306 { 307 // syntax error, no closing paren 308 enum line = "$random( bottles of beer on the wall"; 309 immutable replaced = line.replaceRandom(); 310 assert((replaced == line), replaced); 311 } 312 { 313 // syntax error, no upper bound given 314 enum line = "$random(0..) bottles of beer on the wall"; 315 immutable replaced = line.replaceRandom(); 316 assert((replaced == line), replaced); 317 } 318 { 319 // syntax error, no boudns given 320 enum line = "$random(..) bottles of beer on the wall"; 321 immutable replaced = line.replaceRandom(); 322 assert((replaced == line), replaced); 323 } 324 { 325 // syntax error, missing closing paren 326 enum line = "$random(0..100 bottles of beer on the wall"; 327 immutable replaced = line.replaceRandom(); 328 assert((replaced == line), replaced); 329 } 330 { 331 // syntax error, parens include text 332 enum line = "$random(0..100 bottles of beer on the wall)"; 333 immutable replaced = line.replaceRandom(); 334 assert((replaced == line), replaced); 335 } 336 { 337 // syntax error, i == n 338 enum line = "$random(0..0) bottles of beer on the wall"; 339 immutable replaced = line.replaceRandom(); 340 assert((replaced == line), replaced); 341 } 342 { 343 // syntax error, i > n 344 enum line = "$random(2..1) bottles of beer on the wall"; 345 immutable replaced = line.replaceRandom(); 346 assert((replaced == line), replaced); 347 } 348 { 349 // empty string 350 enum line = string.init; 351 string replaced = line.replaceRandom(); 352 assert(!replaced.length, replaced); 353 } 354 { 355 // no $random token 356 enum line = "99 bottles of beer on the wall"; 357 immutable replaced = line.replaceRandom(); 358 assert((replaced == line), replaced); 359 } 360 } 361 362 363 // doublyBackslashed 364 /++ 365 Returns the supplied string with any backslashes doubled. This is to make 366 paths on Windows display properly. 367 368 Merely returns the given string on Posix and other non-Windows platforms. 369 370 Example: 371 --- 372 string path = r"c:\Windows\system32"; 373 assert(path.doublyBackslashed == r"c:\\Windows\\system32"); 374 --- 375 376 Params: 377 path = The original path string with only single backslashes. 378 379 Returns: 380 The passed `path` but doubly backslashed. 381 +/ 382 auto doublyBackslashed(const string path) 383 { 384 if (!path.length) return path; 385 386 version(Windows) 387 { 388 import std.array : replace; 389 import std.string : indexOf; 390 391 string slice = path.replace('\\', r"\\"); 392 393 while (slice.indexOf(r"\\\\") != -1) 394 { 395 slice = slice.replace(r"\\\\", r"\\"); 396 } 397 398 return slice; 399 } 400 else 401 { 402 return path; 403 } 404 } 405 406 /// 407 version(Windows) 408 unittest 409 { 410 { 411 enum path = r"c:\windows\system32"; 412 enum expected = r"c:\\windows\\system32"; 413 immutable actual = path.doublyBackslashed; 414 assert((actual == expected), actual); 415 } 416 { 417 enum path = r"c:\Users\blerp\AppData\Local\kameloso\server\irc.chat.twitch.tv"; 418 enum expected = r"c:\\Users\\blerp\\AppData\\Local\\kameloso\\server\\irc.chat.twitch.tv"; 419 immutable actual = path.doublyBackslashed; 420 assert((actual == expected), actual); 421 } 422 { 423 enum path = r"c:\\windows\\system32"; 424 enum expected = r"c:\\windows\\system32"; 425 immutable actual = path.doublyBackslashed; 426 assert((actual == expected), actual); 427 } 428 }