A set of classes for parsing, evaluating, and formatting die roll strings.
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

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