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

SA_DiceParser.m 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  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. return [self legacyExpressionForStringDescribingOperation:dieRollString withOperator:operator atRange:lastOperatorRange];
  153. }
  154. // If not an operation, the top-level term might be a die roll command.
  155. // Look for one of the characters recognized as valid die roll delimiters.
  156. // Note that we parse roll commands left-associatively, therefore e.g.
  157. // 5d6d10 parses as "roll N d10s, where N is the result of rolling 5d6".
  158. NSRange lastRollCommandDelimiterRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser validRollCommandDelimiterCharacters]] options:NSBackwardsSearch];
  159. if(lastRollCommandDelimiterRange.location != NSNotFound)
  160. {
  161. return [self legacyExpressionForStringDescribingRollCommand:dieRollString withDelimiterAtRange:lastRollCommandDelimiterRange];
  162. }
  163. // If not an operation nor a roll command, the top-level term can only be
  164. // a simple numeric value.
  165. return [self legacyExpressionForStringDescribingNumericValue:dieRollString];
  166. }
  167. - (NSDictionary *)legacyExpressionForStringDescribingOperation:(NSString *)dieRollString withOperator:(NSString *)operator atRange:(NSRange)operatorRange
  168. {
  169. NSMutableDictionary *expression;
  170. // If the operator is at the beginning, this may be negation; we handle that
  171. // case later below. Otherwise, it's a binary operator.
  172. if(operatorRange.location != 0)
  173. {
  174. expression = [NSMutableDictionary dictionary];
  175. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
  176. expression[SA_DB_INPUT_STRING] = dieRollString;
  177. // Operands of a binary operator are the expressions generated by
  178. // parsing the strings before and after the addition operator.
  179. expression[SA_DB_OPERAND_LEFT] = [self legacyExpressionForLegalString:[dieRollString substringToIndex:operatorRange.location]];
  180. expression[SA_DB_OPERAND_RIGHT] = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(operatorRange.location + operatorRange.length)]];
  181. // Check to see if the term is a subtraction operation.
  182. if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS] containsCharactersInString:operator])
  183. {
  184. expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_MINUS;
  185. }
  186. // Check to see if the term is an addition operation.
  187. else if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_PLUS] containsCharactersInString:operator])
  188. {
  189. expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_PLUS;
  190. }
  191. // Check to see if the term is a multiplication operation.
  192. else if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_TIMES] containsCharactersInString:operator])
  193. {
  194. // Look for other, lower-precedence operators to the left of the
  195. // multiplication operator. If found, split the string there
  196. // instead of at the current operator.
  197. NSString *allLowerPrecedenceOperators = [NSString stringWithFormat:@"%@%@", [SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_PLUS], [SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS]];
  198. NSRange lastLowerPrecedenceOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:allLowerPrecedenceOperators] options:NSBackwardsSearch range:NSMakeRange(1, operatorRange.location - 1)];
  199. if(lastLowerPrecedenceOperatorRange.location != NSNotFound)
  200. {
  201. NSString *lowerPrecedenceOperator = [dieRollString substringWithRange:lastLowerPrecedenceOperatorRange];
  202. return [self legacyExpressionForStringDescribingOperation:dieRollString withOperator:lowerPrecedenceOperator atRange:lastLowerPrecedenceOperatorRange];
  203. }
  204. expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_TIMES;
  205. }
  206. else
  207. {
  208. addErrorToExpression(SA_DB_ERROR_UNKNOWN_OPERATOR, expression);
  209. }
  210. }
  211. // Check to see if the term is a negation operation (we can only reach this
  212. // case if the subtraction operator is at the beginning).
  213. else
  214. {
  215. if(dieRollString.length == operatorRange.length)
  216. {
  217. expression = [NSMutableDictionary dictionary];
  218. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
  219. expression[SA_DB_INPUT_STRING] = dieRollString;
  220. addErrorToExpression(SA_DB_ERROR_INVALID_EXPRESSION, expression);
  221. return expression;
  222. }
  223. if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS] containsCharactersInString:operator])
  224. {
  225. // It turns out this isn't actually an operation expression. It's a
  226. // simple value expression with a negative number.
  227. // This next line is a hack to account for the fact that Cocoa's
  228. // Unicode compliance is incomplete. :( NSString's integerValue
  229. // method only accepts the hyphen as a negation sign when reading a
  230. // number - not any of the Unicode characters which officially
  231. // symbolize negation! But we are more modern-minded, and accept
  232. // arbitrary symbols as minus-sign. For proper parsing, though,
  233. // we have to replace it like this...
  234. NSString *fixedRollString = [dieRollString stringByReplacingCharactersInRange:operatorRange withString:@"-"];
  235. return [self legacyExpressionForStringDescribingNumericValue:fixedRollString];
  236. }
  237. else
  238. {
  239. expression = [NSMutableDictionary dictionary];
  240. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
  241. expression[SA_DB_INPUT_STRING] = dieRollString;
  242. addErrorToExpression(SA_DB_ERROR_INVALID_EXPRESSION, expression);
  243. return expression;
  244. }
  245. }
  246. // The operands have now been parsed recursively; this parsing may have
  247. // generated one or more errors. Inherit any error(s) from the
  248. // error-generating operand(s).
  249. addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_RIGHT], expression);
  250. addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_LEFT], expression);
  251. return expression;
  252. }
  253. - (NSDictionary *)legacyExpressionForStringDescribingRollCommand:(NSString *)dieRollString withDelimiterAtRange:(NSRange)delimiterRange
  254. {
  255. NSMutableDictionary *expression = [NSMutableDictionary dictionary];
  256. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_ROLL_COMMAND;
  257. expression[SA_DB_INPUT_STRING] = dieRollString;
  258. // For now, only one kind of roll command is supported - roll-and-sum.
  259. // This rolls one or more dice of a given sort, and determines the sum of
  260. // their rolled values.
  261. // In the future, support for other, more complex roll commands might be
  262. // added, such as "roll several and return the highest", exploding dice,
  263. // etc.
  264. expression[SA_DB_ROLL_COMMAND] = SA_DB_ROLL_COMMAND_SUM;
  265. // Check to see if the delimiter is the initial character of the roll
  266. // string. If so (i.e. if the die count is omitted), we assume it to be 1
  267. // (i.e. 'd6' is read as '1d6').
  268. if(delimiterRange.location == 0)
  269. {
  270. expression[SA_DB_ROLL_DIE_COUNT] = [self legacyExpressionForStringDescribingNumericValue:@"1"];
  271. }
  272. else
  273. {
  274. // The die count is the expression generated by parsing the string
  275. // before the delimiter.
  276. expression[SA_DB_ROLL_DIE_COUNT] = [self legacyExpressionForLegalString:[dieRollString substringToIndex:delimiterRange.location]];
  277. }
  278. // The die size is the expression generated by parsing the string after the
  279. // delimiter.
  280. expression[SA_DB_ROLL_DIE_SIZE] = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(delimiterRange.location + delimiterRange.length)]];
  281. // The die count and die size have now been parsed recursively; this parsing
  282. // may have generated one or more errors. Inherit any error(s) from the
  283. // error-generating sub-terms.
  284. addErrorsFromExpressionToExpression(expression[SA_DB_ROLL_DIE_COUNT], expression);
  285. addErrorsFromExpressionToExpression(expression[SA_DB_ROLL_DIE_SIZE], expression);
  286. return expression;
  287. }
  288. - (NSDictionary *)legacyExpressionForStringDescribingNumericValue:(NSString *)dieRollString
  289. {
  290. NSMutableDictionary *expression = [NSMutableDictionary dictionary];
  291. expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_VALUE;
  292. expression[SA_DB_INPUT_STRING] = dieRollString;
  293. expression[SA_DB_VALUE] = @(dieRollString.integerValue);
  294. return expression;
  295. }
  296. /****************************/
  297. #pragma mark - Helper methods
  298. /****************************/
  299. + (void)loadValidCharactersDict
  300. {
  301. NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME ofType:@"plist"];
  302. _validCharactersDict = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath][SA_DB_VALID_CHARACTERS];
  303. if(!_validCharactersDict)
  304. {
  305. NSLog(@"Could not load valid characters dictionary!");
  306. }
  307. }
  308. + (NSString *)allValidCharacters
  309. {
  310. NSMutableString *validCharactersString = [NSMutableString string];
  311. [validCharactersString appendString:[SA_DiceParser validNumeralCharacters]];
  312. [validCharactersString appendString:[SA_DiceParser validRollCommandDelimiterCharacters]];
  313. [validCharactersString appendString:[SA_DiceParser allValidOperatorCharacters]];
  314. return validCharactersString;
  315. }
  316. + (NSString *)allValidOperatorCharacters
  317. {
  318. NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
  319. __block NSMutableString *validOperatorCharactersString = [NSMutableString string];
  320. [validCharactersDict[SA_DB_VALID_OPERATOR_CHARACTERS] enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) {
  321. [validOperatorCharactersString appendString:value];
  322. }];
  323. return validOperatorCharactersString;
  324. }
  325. + (NSString *)validCharactersForOperator:(NSString *)operatorName
  326. {
  327. return [SA_DiceParser validCharactersDict][SA_DB_VALID_OPERATOR_CHARACTERS][operatorName];
  328. }
  329. + (NSString *)validNumeralCharacters
  330. {
  331. NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
  332. return validCharactersDict[SA_DB_VALID_NUMERAL_CHARACTERS];
  333. }
  334. + (NSString *)validRollCommandDelimiterCharacters
  335. {
  336. NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
  337. return validCharactersDict[SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS];
  338. }
  339. @end