A set of classes for parsing, evaluating, and formatting die roll strings.
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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