A set of classes for parsing, evaluating, and formatting die roll strings.
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

SA_DiceParser.m 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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(@"Valid characters dictionary loaded successfully.");
  306. }
  307. else
  308. {
  309. NSLog(@"Could not load valid characters dictionary!");
  310. }
  311. }
  312. + (NSString *)allValidCharacters
  313. {
  314. NSMutableString *validCharactersString = [NSMutableString string];
  315. [validCharactersString appendString:[SA_DiceParser validNumeralCharacters]];
  316. [validCharactersString appendString:[SA_DiceParser validRollCommandDelimiterCharacters]];
  317. [validCharactersString appendString:[SA_DiceParser allValidOperatorCharacters]];
  318. return validCharactersString;
  319. }
  320. + (NSString *)allValidOperatorCharacters
  321. {
  322. NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
  323. __block NSMutableString *validOperatorCharactersString = [NSMutableString string];
  324. [validCharactersDict[SA_DB_VALID_OPERATOR_CHARACTERS] enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) {
  325. [validOperatorCharactersString appendString:value];
  326. }];
  327. return validOperatorCharactersString;
  328. }
  329. + (NSString *)validCharactersForOperator:(NSString *)operatorName
  330. {
  331. return [SA_DiceParser validCharactersDict][SA_DB_VALID_OPERATOR_CHARACTERS][operatorName];
  332. }
  333. + (NSString *)validNumeralCharacters
  334. {
  335. NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
  336. return validCharactersDict[SA_DB_VALID_NUMERAL_CHARACTERS];
  337. }
  338. + (NSString *)validRollCommandDelimiterCharacters
  339. {
  340. NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
  341. return validCharactersDict[SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS];
  342. }
  343. @end