A set of classes for parsing, evaluating, and formatting die roll strings.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

SA_DiceParser.m 15KB

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