A set of classes for parsing, evaluating, and formatting die roll strings.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

SA_DiceParser.m 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. //
  2. // SA_DiceParser.m
  3. //
  4. // Copyright (c) 2016 Said Achmiz.
  5. //
  6. // This software is licensed under the MIT license.
  7. // See the file "LICENSE" for more information.
  8. #import "SA_DiceParser.h"
  9. #import "SA_DiceExpressionStringConstants.h"
  10. #import "SA_DiceErrorHandling.h"
  11. #import "NSString+SA_NSStringExtensions.h"
  12. #import "SA_DiceFormatter.h"
  13. /********************************/
  14. #pragma mark File-scope variables
  15. /********************************/
  16. static SA_DiceParserBehavior _defaultParserBehavior = SA_DiceParserBehaviorLegacy;
  17. static NSDictionary *_validCharactersDict;
  18. /************************************************/
  19. #pragma mark - SA_DiceParser class implementation
  20. /************************************************/
  21. @implementation SA_DiceParser
  22. {
  23. SA_DiceParserBehavior _parserBehavior;
  24. }
  25. /************************/
  26. #pragma mark - Properties
  27. /************************/
  28. - (void)setParserBehavior:(SA_DiceParserBehavior)newParserBehavior
  29. {
  30. _parserBehavior = newParserBehavior;
  31. switch (_parserBehavior)
  32. {
  33. case SA_DiceParserBehaviorLegacy:
  34. case SA_DiceParserBehaviorModern:
  35. case SA_DiceParserBehaviorFeepbot:
  36. break;
  37. case SA_DiceParserBehaviorDefault:
  38. default:
  39. _parserBehavior = [SA_DiceParser defaultParserBehavior];
  40. break;
  41. }
  42. }
  43. - (SA_DiceParserBehavior)parserBehavior
  44. {
  45. return _parserBehavior;
  46. }
  47. /****************************************/
  48. #pragma mark - "Class property" accessors
  49. /****************************************/
  50. + (void)setDefaultParserBehavior:(SA_DiceParserBehavior)newDefaultParserBehavior
  51. {
  52. if(newDefaultParserBehavior == SA_DiceParserBehaviorDefault)
  53. {
  54. _defaultParserBehavior = SA_DiceParserBehaviorLegacy;
  55. }
  56. else
  57. {
  58. _defaultParserBehavior = newDefaultParserBehavior;
  59. }
  60. }
  61. + (SA_DiceParserBehavior)defaultParserBehavior
  62. {
  63. return _defaultParserBehavior;
  64. }
  65. + (NSDictionary *)validCharactersDict
  66. {
  67. if(_validCharactersDict == nil)
  68. {
  69. [SA_DiceParser loadValidCharactersDict];
  70. }
  71. return _validCharactersDict;
  72. }
  73. /********************************************/
  74. #pragma mark - Initializers & factory methods
  75. /********************************************/
  76. - (instancetype)init
  77. {
  78. return [self initWithBehavior:SA_DiceParserBehaviorDefault];
  79. }
  80. - (instancetype)initWithBehavior:(SA_DiceParserBehavior)parserBehavior
  81. {
  82. if(self = [super init])
  83. {
  84. self.parserBehavior = parserBehavior;
  85. if(_validCharactersDict == nil)
  86. {
  87. [SA_DiceParser loadValidCharactersDict];
  88. }
  89. }
  90. return self;
  91. }
  92. + (instancetype)defaultParser
  93. {
  94. return [[SA_DiceParser alloc] initWithBehavior:SA_DiceParserBehaviorDefault];
  95. }
  96. + (instancetype)parserWithBehavior:(SA_DiceParserBehavior)parserBehavior
  97. {
  98. return [[SA_DiceParser alloc] initWithBehavior:parserBehavior];
  99. }
  100. /****************************/
  101. #pragma mark - Public methods
  102. /****************************/
  103. - (NSDictionary *)expressionForString:(NSString *)dieRollString
  104. {
  105. if(_parserBehavior == SA_DiceParserBehaviorLegacy)
  106. {
  107. return [self legacyExpressionForString:dieRollString];
  108. }
  109. else
  110. {
  111. return @{};
  112. }
  113. }
  114. - (NSDictionary *)expressionByJoiningExpression:(NSDictionary *)leftHandExpression toExpression:(NSDictionary *)rightHandExpression withOperator:(NSString *)operatorName
  115. {
  116. NSMutableDictionary *expression = [NSMutableDictionary dictionary];
  117. // First, we check that the operands and operator are not nil. If they are,
  118. // then the expression is invalid...
  119. if(leftHandExpression == nil || rightHandExpression == nil || operatorName == nil)
  120. {
  121. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_NONE;
  122. addErrorToExpression(SA_DB_ERROR_INVALID_EXPRESSION, expression);
  123. return expression;
  124. }
  125. // If the operands and operator are present, then the expression is an
  126. // operation expression...
  127. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
  128. // ... but does it have a valid operator?
  129. if([operatorName isEqualToString:SA_DB_OPERATOR_PLUS] ||
  130. [operatorName isEqualToString:SA_DB_OPERATOR_MINUS] ||
  131. [operatorName isEqualToString:SA_DB_OPERATOR_TIMES]
  132. )
  133. {
  134. expression[SA_DB_OPERATOR] = operatorName;
  135. }
  136. else
  137. {
  138. addErrorToExpression(SA_DB_ERROR_UNKNOWN_OPERATOR, expression);
  139. return expression;
  140. }
  141. // The operator is valid. Set the operands...
  142. expression[SA_DB_OPERAND_LEFT] = leftHandExpression;
  143. expression[SA_DB_OPERAND_RIGHT] = rightHandExpression;
  144. // And inherit any errors that they may have.
  145. addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_LEFT], expression);
  146. addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_RIGHT], expression);
  147. // Since this top-level expression was NOT generated by parsing an input
  148. // string, for completeness and consistency, we have to generate a fake
  149. // input string ourselves! We do this by wrapping each operand in
  150. // parentheses and putting the canonical representation of the operator
  151. // between them.
  152. NSString *fakeInputString = [NSString stringWithFormat:@"(%@)%@(%@)",
  153. expression[SA_DB_OPERAND_LEFT][SA_DB_INPUT_STRING],
  154. [SA_DiceFormatter canonicalRepresentationForOperator:expression[SA_DB_OPERATOR]],
  155. expression[SA_DB_OPERAND_RIGHT][SA_DB_INPUT_STRING]];
  156. expression[SA_DB_INPUT_STRING] = fakeInputString;
  157. // The joining is complete. (Power overwhelming.)
  158. return expression;
  159. }
  160. /**********************************************/
  161. #pragma mark - "Legacy" behavior implementation
  162. /**********************************************/
  163. - (NSDictionary *)legacyExpressionForString:(NSString *)dieRollString
  164. {
  165. // Check for forbidden characters.
  166. NSCharacterSet *forbiddenCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidCharacters]] invertedSet];
  167. if([dieRollString containsCharactersInSet:forbiddenCharacterSet])
  168. {
  169. return @{ SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_NONE,
  170. SA_DB_INPUT_STRING : dieRollString,
  171. SA_DB_ERRORS : @[SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS] };
  172. }
  173. // Since we have checked the entire string for forbidden characters, we can
  174. // now begin parsing the string; there is no need to check substrings for
  175. // illegal characters (which is why we do it only once, in this wrapper
  176. // method). When constructing the expression tree, we call
  177. // legacyExpressionForLegalString:, not legacyExpressionForString:, when
  178. // recursively parsing substrings.
  179. return [self legacyExpressionForLegalString:dieRollString];
  180. }
  181. - (NSDictionary *)legacyExpressionForLegalString:(NSString *)dieRollString
  182. {
  183. // Make sure string is not empty.
  184. if(dieRollString.length == 0)
  185. {
  186. return @{ SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_NONE,
  187. SA_DB_INPUT_STRING : dieRollString,
  188. SA_DB_ERRORS : @[SA_DB_ERROR_ROLL_STRING_EMPTY] };
  189. }
  190. // We now know the string describes one of the allowable expression types
  191. // (probably; it could be malformed in some way other than being empty or
  192. // containing forbidden characters, such as e.g. by starting with a + sign).
  193. // Check to see if the top-level term is an operation. Note that we parse
  194. // operator expressions left-associatively.
  195. NSRange lastOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidOperatorCharacters]] options:NSBackwardsSearch];
  196. if(lastOperatorRange.location != NSNotFound)
  197. {
  198. NSString *operator = [dieRollString substringWithRange:lastOperatorRange];
  199. // If the last (and thus only) operator is the leading character of
  200. // the expression, then this is one of several possible special cases.
  201. if(lastOperatorRange.location == 0)
  202. {
  203. NSMutableDictionary *expression;
  204. // First, we check for whether there even is anything more to the
  205. // roll string besides the operator. If not, then the string is
  206. // malformed by definition...
  207. if(dieRollString.length == lastOperatorRange.length)
  208. {
  209. expression = [NSMutableDictionary dictionary];
  210. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
  211. expression[SA_DB_INPUT_STRING] = dieRollString;
  212. addErrorToExpression(SA_DB_ERROR_INVALID_EXPRESSION, expression);
  213. return expression;
  214. }
  215. // If the last operator is the leading character (i.e. there's just
  216. // one operator in the expression, and it's at the beginning), and
  217. // there's more to the expression than just the operator, then
  218. // this is either an expression whose first term (which may or may
  219. // not be its only term) is a simple value expression which
  220. // represents a negative number - or, it's a malformed expression
  221. // (because operators other than negation cannot begin an
  222. // expression).
  223. // In the former case, we do nothing, letting the testing for
  224. // expression type fall through to the remaining cases (roll command
  225. // or simple value).
  226. // In the latter case, we register an error and return.
  227. if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS] containsCharactersInString:operator])
  228. {
  229. // We've determined that this expression begins with a simple
  230. // value expression that represents a negative number.
  231. // This next line is a hack to account for the fact that Cocoa's
  232. // Unicode compliance is incomplete. :( NSString's integerValue
  233. // method only accepts the hyphen as a negation sign when reading a
  234. // number - not any of the Unicode characters which officially
  235. // symbolize negation! But we are more modern-minded, and accept
  236. // arbitrary symbols as minus-sign. For proper parsing, though,
  237. // we have to replace it like this...
  238. dieRollString = [dieRollString stringByReplacingCharactersInRange:lastOperatorRange withString:@"-"];
  239. // Now we skip the remainder of the "is it an operator?" code
  240. // and fall through to "is it a roll command, or maybe a simple
  241. // value?"...
  242. }
  243. else
  244. {
  245. expression = [NSMutableDictionary dictionary];
  246. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
  247. expression[SA_DB_INPUT_STRING] = dieRollString;
  248. addErrorToExpression(SA_DB_ERROR_INVALID_EXPRESSION, expression);
  249. return expression;
  250. }
  251. }
  252. else
  253. {
  254. return [self legacyExpressionForStringDescribingOperation:dieRollString withOperator:operator atRange:lastOperatorRange];
  255. }
  256. }
  257. // If not an operation, the top-level term might be a die roll command.
  258. // Look for one of the characters recognized as valid die roll delimiters.
  259. // Note that we parse roll commands left-associatively, therefore e.g.
  260. // 5d6d10 parses as "roll N d10s, where N is the result of rolling 5d6".
  261. NSRange lastRollCommandDelimiterRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser validRollCommandDelimiterCharacters]] options:NSBackwardsSearch];
  262. if(lastRollCommandDelimiterRange.location != NSNotFound)
  263. {
  264. return [self legacyExpressionForStringDescribingRollCommand:dieRollString withDelimiterAtRange:lastRollCommandDelimiterRange];
  265. }
  266. // If not an operation nor a roll command, the top-level term can only be
  267. // a simple numeric value.
  268. return [self legacyExpressionForStringDescribingNumericValue:dieRollString];
  269. }
  270. - (NSDictionary *)legacyExpressionForStringDescribingOperation:(NSString *)dieRollString withOperator:(NSString *)operator atRange:(NSRange)operatorRange
  271. {
  272. NSMutableDictionary *expression;
  273. expression = [NSMutableDictionary dictionary];
  274. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
  275. expression[SA_DB_INPUT_STRING] = dieRollString;
  276. // Operands of a binary operator are the expressions generated by
  277. // parsing the strings before and after the addition operator.
  278. expression[SA_DB_OPERAND_LEFT] = [self legacyExpressionForLegalString:[dieRollString substringToIndex:operatorRange.location]];
  279. expression[SA_DB_OPERAND_RIGHT] = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(operatorRange.location + operatorRange.length)]];
  280. // Check to see if the term is an addition operation.
  281. if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_PLUS] containsCharactersInString:operator])
  282. {
  283. expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_PLUS;
  284. }
  285. // Check to see if the term is a subtraction operation.
  286. else if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS] containsCharactersInString:operator])
  287. {
  288. expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_MINUS;
  289. }
  290. // Check to see if the term is a multiplication operation.
  291. else if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_TIMES] containsCharactersInString:operator])
  292. {
  293. // Look for other, lower-precedence operators to the left of the
  294. // multiplication operator. If found, split the string there
  295. // instead of at the current operator.
  296. NSString *allLowerPrecedenceOperators = [NSString stringWithFormat:@"%@%@", [SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_PLUS], [SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS]];
  297. NSRange lastLowerPrecedenceOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:allLowerPrecedenceOperators] options:NSBackwardsSearch range:NSMakeRange(1, operatorRange.location - 1)];
  298. if(lastLowerPrecedenceOperatorRange.location != NSNotFound)
  299. {
  300. NSString *lowerPrecedenceOperator = [dieRollString substringWithRange:lastLowerPrecedenceOperatorRange];
  301. return [self legacyExpressionForStringDescribingOperation:dieRollString withOperator:lowerPrecedenceOperator atRange:lastLowerPrecedenceOperatorRange];
  302. }
  303. expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_TIMES;
  304. }
  305. else
  306. {
  307. addErrorToExpression(SA_DB_ERROR_UNKNOWN_OPERATOR, expression);
  308. }
  309. // The operands have now been parsed recursively; this parsing may have
  310. // generated one or more errors. Inherit any error(s) from the
  311. // error-generating operand(s).
  312. addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_RIGHT], expression);
  313. addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_LEFT], expression);
  314. return expression;
  315. }
  316. - (NSDictionary *)legacyExpressionForStringDescribingRollCommand:(NSString *)dieRollString withDelimiterAtRange:(NSRange)delimiterRange
  317. {
  318. NSMutableDictionary *expression = [NSMutableDictionary dictionary];
  319. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_ROLL_COMMAND;
  320. expression[SA_DB_INPUT_STRING] = dieRollString;
  321. // For now, only one kind of roll command is supported - roll-and-sum.
  322. // This rolls one or more dice of a given sort, and determines the sum of
  323. // their rolled values.
  324. // In the future, support for other, more complex roll commands might be
  325. // added, such as "roll several and return the highest", exploding dice,
  326. // etc.
  327. expression[SA_DB_ROLL_COMMAND] = SA_DB_ROLL_COMMAND_SUM;
  328. // Check to see if the delimiter is the initial character of the roll
  329. // string. If so (i.e. if the die count is omitted), we assume it to be 1
  330. // (i.e. 'd6' is read as '1d6').
  331. if(delimiterRange.location == 0)
  332. {
  333. expression[SA_DB_ROLL_DIE_COUNT] = [self legacyExpressionForStringDescribingNumericValue:@"1"];
  334. }
  335. else
  336. {
  337. // The die count is the expression generated by parsing the string
  338. // before the delimiter.
  339. expression[SA_DB_ROLL_DIE_COUNT] = [self legacyExpressionForLegalString:[dieRollString substringToIndex:delimiterRange.location]];
  340. }
  341. // The die size is the expression generated by parsing the string after the
  342. // delimiter.
  343. expression[SA_DB_ROLL_DIE_SIZE] = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(delimiterRange.location + delimiterRange.length)]];
  344. // The die count and die size have now been parsed recursively; this parsing
  345. // may have generated one or more errors. Inherit any error(s) from the
  346. // error-generating sub-terms.
  347. addErrorsFromExpressionToExpression(expression[SA_DB_ROLL_DIE_COUNT], expression);
  348. addErrorsFromExpressionToExpression(expression[SA_DB_ROLL_DIE_SIZE], expression);
  349. return expression;
  350. }
  351. - (NSDictionary *)legacyExpressionForStringDescribingNumericValue:(NSString *)dieRollString
  352. {
  353. NSMutableDictionary *expression = [NSMutableDictionary dictionary];
  354. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_VALUE;
  355. expression[SA_DB_INPUT_STRING] = dieRollString;
  356. expression[SA_DB_VALUE] = @(dieRollString.integerValue);
  357. return expression;
  358. }
  359. /****************************/
  360. #pragma mark - Helper methods
  361. /****************************/
  362. + (void)loadValidCharactersDict
  363. {
  364. NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME ofType:@"plist"];
  365. _validCharactersDict = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath][SA_DB_VALID_CHARACTERS];
  366. if(!_validCharactersDict)
  367. {
  368. NSLog(@"Could not load valid characters dictionary!");
  369. }
  370. }
  371. + (NSString *)allValidCharacters
  372. {
  373. NSMutableString *validCharactersString = [NSMutableString string];
  374. [validCharactersString appendString:[SA_DiceParser validNumeralCharacters]];
  375. [validCharactersString appendString:[SA_DiceParser validRollCommandDelimiterCharacters]];
  376. [validCharactersString appendString:[SA_DiceParser allValidOperatorCharacters]];
  377. return validCharactersString;
  378. }
  379. + (NSString *)allValidOperatorCharacters
  380. {
  381. NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
  382. __block NSMutableString *validOperatorCharactersString = [NSMutableString string];
  383. [validCharactersDict[SA_DB_VALID_OPERATOR_CHARACTERS] enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) {
  384. [validOperatorCharactersString appendString:value];
  385. }];
  386. return validOperatorCharactersString;
  387. }
  388. + (NSString *)validCharactersForOperator:(NSString *)operatorName
  389. {
  390. return [SA_DiceParser validCharactersDict][SA_DB_VALID_OPERATOR_CHARACTERS][operatorName];
  391. }
  392. + (NSString *)validNumeralCharacters
  393. {
  394. NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
  395. return validCharactersDict[SA_DB_VALID_NUMERAL_CHARACTERS];
  396. }
  397. + (NSString *)validRollCommandDelimiterCharacters
  398. {
  399. NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
  400. return validCharactersDict[SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS];
  401. }
  402. @end