1 /** 2 Implements QoL functions to handle user input-output. 3 4 Unless otherwise stated, all methods make use of `stdin` and 5 `stdout` for input and output, respectively, or this thread's 6 running `ITextMenu`, if any. 7 */ 8 module dli.io; 9 10 import dli.text_menu; 11 import std.conv; 12 import std.exception; 13 import std.meta; 14 import std.stdio : stdin, stdout; 15 import std..string : chomp, format; 16 import std.traits; 17 18 /** 19 Requests a text confirmation from the user. 20 21 Returns: whether or not the user input, stripped of line terminators, 22 exactly matches requiredAnswer. 23 */ 24 public bool requestConfirmation(string requestMsg, string requiredAnswer) 25 in 26 { 27 assert(requestMsg !is null); 28 assert(requiredAnswer !is null); 29 } 30 body 31 { 32 import std..string : strip; 33 34 string answer; 35 return request!string(requestMsg, &answer, (string s){return s == requiredAnswer;}); 36 } 37 38 /// Types supported by the helper 'request' method 39 private alias requestSupportedTypes = AliasSeq!( 40 ubyte, 41 ushort, 42 uint, 43 ulong, 44 byte, 45 short, 46 int, 47 long, 48 char, 49 // wchar, not supported until to!(wchar, string) is available in Phobos 50 dchar, 51 float, 52 double, 53 real, 54 string, 55 wstring, 56 dstring 57 ); 58 59 /** 60 Requests data with the possibility of adding restrictions. 61 User input is stripped of the line terminator before processing is attempted. 62 63 Params: requestMsg = message to write out when asking for data. 64 dataDestination = pointer where the input data is to be stored, 65 if the input can be converted and satifsties 66 restriction. 67 restriction = a callable item that takes a single dataT argument 68 and returns whether it is valid or not. Use it to 69 add additional restrictions onto the data being 70 requested. 71 72 Returns: whether or not the input data is valid. If `false`, no writing has been 73 performed into dataDestination. 74 */ 75 public bool request(dataT, restrictionCheckerT) 76 (string requestMsg, 77 dataT* dataDestination, 78 restrictionCheckerT restriction = (dataT foo){return true;}, // No restrictions by default 79 ) 80 if(staticIndexOf!(dataT, requestSupportedTypes) != -1 && 81 isCallable!restrictionCheckerT && 82 Parameters!restrictionCheckerT.length == 1 && 83 is(Parameters!restrictionCheckerT[0] : dataT) && 84 is(ReturnType!restrictionCheckerT == bool)) 85 in 86 { 87 assert(requestMsg !is null); 88 assert(dataDestination !is null); 89 assert(restriction !is null); 90 } 91 body 92 { 93 string input; 94 if(activeTextMenu is null) 95 { 96 stdout.write(requestMsg); 97 input = stdin.readln().chomp(); 98 } 99 else 100 { 101 activeTextMenu.write(requestMsg); 102 input = activeTextMenu.readln().chomp(); 103 } 104 105 if (input is null) 106 return false; 107 108 version(MathExpressionSupport) 109 enum supportMathExpr = true; 110 else 111 enum supportMathExpr = false; 112 113 try 114 { 115 dataT data = void; 116 static if (supportMathExpr && isNumeric!dataT) 117 { 118 import std..string : isNumeric; 119 if (input.isNumeric) 120 data = to!dataT(input); 121 else // If the input is not numeric, try to evaluate as a math expression 122 { 123 import arith_eval : Evaluable; 124 125 auto evaluable = Evaluable!()(input); 126 static if (isFloatingPoint!dataT) 127 data = evaluable.eval(); 128 else 129 { 130 import std.math : abs, round; 131 132 immutable float floatResult = evaluable.eval(); 133 immutable float closestInteger = round(floatResult); 134 enum validThreshold = 0.01f; 135 if (abs(floatResult - closestInteger) <= validThreshold) 136 data = to!dataT(evaluable.eval()); 137 else 138 throw new Exception( 139 format("Evaluated input \"%s\" is too far from nearest integer %s " ~ 140 "to be considered a valid integer input.") 141 ); 142 } 143 } 144 } 145 else 146 data = to!dataT(input); 147 148 if(restriction(data)) 149 { 150 *dataDestination = data; 151 return true; 152 } 153 } 154 catch(Exception e) 155 { 156 } 157 158 return false; 159 } 160 161 /** 162 Writes s to the output. 163 164 Params: s = string to write. 165 */ 166 public void write(string s) 167 in 168 { 169 assert(s !is null); 170 } 171 body 172 { 173 if(activeTextMenu is null) 174 stdout.write(s); 175 else 176 activeTextMenu.write(s); 177 } 178 179 /** 180 Writes s, plus a line terminator, to the output. 181 182 Params: s = string to write. 183 */ 184 public void writeln(string s) 185 in 186 { 187 assert(s !is null); 188 } 189 body 190 { 191 if(activeTextMenu is null) 192 stdout.writeln(s); 193 else 194 activeTextMenu.writeln(s); 195 } 196 197 // TESTS 198 version(unittest) 199 { 200 import std.exception; 201 import test.dli.mock_menu; 202 import test.dli.mock_menu_item; 203 import unit_threaded; 204 205 version = MathExpressionSupport; 206 207 @("requestConfirmation works if called from a running ITextMenu") 208 unittest 209 { 210 auto menu = new MockMenu(); 211 immutable string confirmationAnswer = "_CONFIRM_"; // Just a random string 212 bool confirmed; 213 214 auto item = new MenuItem("", 215 { 216 confirmed = requestConfirmation("", confirmationAnswer); 217 } 218 ); 219 220 menu.addItem(item, 1); 221 222 menu.mock_writeln("1"); 223 menu.mock_writeln("asdf"); // Whatever different from confirmationAnswer 224 menu.mock_writeExitRequest(); 225 menu.run(); 226 227 assert(!confirmed); 228 229 menu.mock_writeln("1"); 230 menu.mock_writeln(confirmationAnswer ~ "a"); // Contains confirmation answer, but does not match 231 menu.mock_writeExitRequest(); 232 menu.run(); 233 234 assert(!confirmed); 235 236 menu.mock_writeln("1"); 237 menu.mock_writeln(confirmationAnswer); 238 menu.mock_writeExitRequest(); 239 menu.run(); 240 241 assert(confirmed); 242 } 243 244 enum isValidInput(string input) = false; 245 enum isValidInput(string input : "a") = true; 246 247 static foreach (alias supportedType; requestSupportedTypes) 248 { 249 @("request works for type " ~ supportedType.stringof) 250 unittest 251 { 252 auto menu = new MockMenu(); 253 254 supportedType myData; 255 bool dataValid; 256 257 menu.addItem( 258 new MenuItem("", 259 {dataValid = request("", &myData);} 260 ), 261 1 262 ); 263 264 enum supportedTypeIsConvertible(T) = is(supportedType : T); 265 266 //enum isValidInput(string input) = false; 267 //enum isValidInput(string input : "a") = true; 268 269 // The user inputs an ASCII character 270 enum charInput = "a"; 271 menu.mock_writeln("1"); 272 menu.mock_writeln(charInput); 273 menu.mock_writeExitRequest(); 274 menu.run(); 275 276 enum charIsValidInput = isSomeChar!supportedType || 277 isSomeString!supportedType; 278 279 assert(dataValid == charIsValidInput); 280 static if (charIsValidInput) 281 assert(myData == to!supportedType(charInput)); 282 283 // The user inputs a double-byte Unicode character 284 enum wcharInput = "á"; 285 menu.mock_writeln("1"); 286 menu.mock_writeln(wcharInput); 287 menu.mock_writeExitRequest(); 288 menu.run(); 289 290 enum wcharIsValidInput = is(supportedType == wchar) || 291 is(supportedType == dchar) || 292 isSomeString!supportedType; 293 294 assert(dataValid == wcharIsValidInput); 295 static if (wcharIsValidInput) 296 assert(myData == to!supportedType(wcharInput)); 297 298 // The user inputs a quadruple-byte Unicode character 299 enum dcharInput = "🙂"; 300 menu.mock_writeln("1"); 301 menu.mock_writeln(dcharInput); 302 menu.mock_writeExitRequest(); 303 menu.run(); 304 305 enum dcharIsValidInput = is(supportedType == dchar) || 306 isSomeString!supportedType; 307 308 assert(dataValid == dcharIsValidInput); 309 static if (dcharIsValidInput) 310 assert(myData == to!supportedType(dcharInput)); 311 312 // The user inputs a general string 313 enum stringInput = "This is English. Esto es español. 這是中國人."; 314 menu.mock_writeln("1"); 315 menu.mock_writeln(stringInput); 316 menu.mock_writeExitRequest(); 317 menu.run(); 318 319 enum stringIsValidInput = isSomeString!supportedType; 320 321 assert(dataValid == stringIsValidInput); 322 static if (stringIsValidInput) 323 assert(myData == stringInput); 324 325 // The user inputs a fractional number 326 enum fractionalInput = "1.23"; 327 menu.mock_writeln("1"); 328 menu.mock_writeln(fractionalInput); 329 menu.mock_writeExitRequest(); 330 menu.run(); 331 332 enum fractionalIsValidInput = isFloatingPoint!supportedType || 333 isSomeString!supportedType; 334 335 dataValid.shouldEqual(fractionalIsValidInput); 336 static if (fractionalIsValidInput) 337 { 338 static if (isSomeString!supportedType) 339 myData.shouldEqual(to!supportedType(fractionalInput)); 340 else static if (isFloatingPoint!supportedType) 341 myData.shouldApproxEqual(to!supportedType(fractionalInput)); 342 } 343 344 // The user inputs a positive integer 345 enum integerInput = "15"; 346 menu.mock_writeln("1"); 347 menu.mock_writeln(integerInput); 348 menu.mock_writeExitRequest(); 349 menu.run(); 350 351 enum positiveIntegerIsValidInput = isNumeric!supportedType || 352 isSomeString!supportedType; 353 354 dataValid.shouldEqual(positiveIntegerIsValidInput); 355 static if (positiveIntegerIsValidInput) 356 myData.shouldEqual(to!supportedType(integerInput)); 357 358 // The user inputs a negative integer 359 enum negativeIntegerInput = "-8"; 360 menu.mock_writeln("1"); 361 menu.mock_writeln(negativeIntegerInput); 362 menu.mock_writeExitRequest(); 363 menu.run(); 364 365 enum negativeIntegerIsValidInput = isSigned!supportedType || 366 isSomeString!supportedType; 367 368 dataValid.shouldEqual(negativeIntegerIsValidInput); 369 static if (negativeIntegerIsValidInput) 370 myData.shouldEqual(to!supportedType(negativeIntegerInput)); 371 372 373 // The user inputs an integer-yielding math expression 374 enum integerYieldingMathExp = "4 / 2"; 375 menu.mock_writeln("1"); 376 menu.mock_writeln(integerYieldingMathExp); 377 menu.mock_writeExitRequest(); 378 menu.run(); 379 380 enum integerYieldingMathExpIsValidInput = 381 isSomeString!supportedType || 382 isNumeric!supportedType; 383 384 dataValid.shouldEqual(integerYieldingMathExpIsValidInput); 385 static if (integerYieldingMathExpIsValidInput) 386 { 387 static if (isSomeString!supportedType) 388 myData.shouldEqual(to!supportedType(integerYieldingMathExp)); 389 else 390 myData.shouldEqual(to!supportedType("2")); // 4 / 2 = 2; 391 } 392 393 // The user inputs an integer-yielding math expression 394 enum floatYieldingMathExp = "1 / 3"; 395 menu.mock_writeln("1"); 396 menu.mock_writeln(floatYieldingMathExp); 397 menu.mock_writeExitRequest(); 398 menu.run(); 399 400 enum floatYieldingMathExpIsValidInput = 401 isSomeString!supportedType || 402 isFloatingPoint!supportedType; 403 404 dataValid.shouldEqual(floatYieldingMathExpIsValidInput); 405 static if (floatYieldingMathExpIsValidInput) 406 { 407 static if (isSomeString!supportedType) 408 myData.shouldEqual(to!supportedType(floatYieldingMathExp)); 409 else 410 myData.shouldApproxEqual(to!supportedType(1f / 3)); 411 } 412 413 } 414 } 415 416 @("request!char accepts whitespace") 417 unittest 418 { 419 auto menu = new MockMenu(); 420 bool dataValid; 421 char inputChar; 422 auto item = new MenuItem("", 423 { 424 dataValid = request("", &inputChar); 425 } 426 ); 427 428 menu.addItem(item, 1); 429 menu.mock_writeln("1"); 430 menu.mock_writeln(" "); 431 menu.mock_writeExitRequest(); 432 menu.run(); 433 434 assert(dataValid); 435 assert(inputChar == ' '); 436 } 437 438 @("request can take restrictions") 439 unittest 440 { 441 int myInt; 442 bool dataValid; 443 auto menu = new MockMenu(); 444 445 menu.addItem( 446 new MenuItem("", 447 { 448 dataValid = request("", &myInt, (int a){return a % 2 == 0;}); // Only accepts even integers 449 } 450 ), 451 1 452 ); 453 454 menu.mock_writeln("1"); 455 menu.mock_writeln("5"); // Not an even integer 456 menu.mock_writeExitRequest(); 457 menu.run(); 458 459 assert(!dataValid); 460 461 menu.mock_writeln("1"); 462 menu.mock_writeln("8"); // Even integer 463 menu.mock_writeExitRequest(); 464 menu.run(); 465 466 assert(dataValid); 467 assert(myInt == 8); 468 } 469 470 @("request supports math expressions") 471 unittest 472 { 473 int userInput; 474 bool dataValid; 475 auto menu = new MockMenu(); 476 477 menu.addItem( 478 new MenuItem("", 479 { 480 dataValid = request("", &userInput, (int a){return a % 2 == 0;}); 481 } 482 ), 1 483 ); 484 485 menu.mock_writeln("1"); 486 menu.mock_writeln("asdf"); 487 menu.mock_writeExitRequest(); 488 menu.run(); 489 490 assert(!dataValid); 491 492 menu.mock_writeln("1"); 493 menu.mock_writeln("1 + 2"); // Not valid, odd value 494 menu.mock_writeExitRequest(); 495 menu.run(); 496 497 assert(!dataValid); 498 499 menu.mock_writeln("1"); 500 menu.mock_writeln("2 + 2"); 501 menu.mock_writeExitRequest(); 502 menu.run(); 503 504 assert(dataValid); 505 assert(userInput == 4); 506 } 507 }