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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  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_DiceFormatter.h"
  11. #import "SA_Utility.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. SA_DiceParserBehavior _parserBehavior;
  22. }
  23. /************************/
  24. #pragma mark - Properties
  25. /************************/
  26. -(void) setParserBehavior:(SA_DiceParserBehavior)newParserBehavior {
  27. _parserBehavior = newParserBehavior;
  28. switch (_parserBehavior) {
  29. case SA_DiceParserBehaviorLegacy:
  30. case SA_DiceParserBehaviorModern:
  31. case SA_DiceParserBehaviorFeepbot:
  32. break;
  33. case SA_DiceParserBehaviorDefault:
  34. default:
  35. _parserBehavior = SA_DiceParser.defaultParserBehavior;
  36. break;
  37. }
  38. }
  39. -(SA_DiceParserBehavior) parserBehavior {
  40. return _parserBehavior;
  41. }
  42. /******************************/
  43. #pragma mark - Class properties
  44. /******************************/
  45. +(void) setDefaultParserBehavior:(SA_DiceParserBehavior)newDefaultParserBehavior {
  46. if (newDefaultParserBehavior == SA_DiceParserBehaviorDefault) {
  47. _defaultParserBehavior = SA_DiceParserBehaviorLegacy;
  48. } else {
  49. _defaultParserBehavior = newDefaultParserBehavior;
  50. }
  51. }
  52. +(SA_DiceParserBehavior) defaultParserBehavior {
  53. return _defaultParserBehavior;
  54. }
  55. // TODO: Should this be on a per-mode, and therefore per-instance, basis?
  56. +(NSDictionary *) validCharactersDict {
  57. if (_validCharactersDict == nil) {
  58. [SA_DiceParser loadValidCharactersDict];
  59. }
  60. return _validCharactersDict;
  61. }
  62. /********************************************/
  63. #pragma mark - Initializers & factory methods
  64. /********************************************/
  65. -(instancetype) init {
  66. return [self initWithBehavior:SA_DiceParserBehaviorDefault];
  67. }
  68. -(instancetype) initWithBehavior:(SA_DiceParserBehavior)parserBehavior {
  69. if (!(self = [super init]))
  70. return nil;
  71. self.parserBehavior = parserBehavior;
  72. if (_validCharactersDict == nil) {
  73. [SA_DiceParser loadValidCharactersDict];
  74. }
  75. return self;
  76. }
  77. +(instancetype) defaultParser {
  78. return [[SA_DiceParser alloc] initWithBehavior:SA_DiceParserBehaviorDefault];
  79. }
  80. +(instancetype) parserWithBehavior:(SA_DiceParserBehavior)parserBehavior {
  81. return [[SA_DiceParser alloc] initWithBehavior:parserBehavior];
  82. }
  83. /****************************/
  84. #pragma mark - Public methods
  85. /****************************/
  86. -(SA_DiceExpression *) expressionForString:(NSString *)dieRollString {
  87. if (_parserBehavior == SA_DiceParserBehaviorLegacy) {
  88. return [self legacyExpressionForString:dieRollString];
  89. } else {
  90. return nil;
  91. }
  92. }
  93. /**********************************************/
  94. #pragma mark - “Legacy” behavior implementation
  95. /**********************************************/
  96. -(SA_DiceExpression *) legacyExpressionForString:(NSString *)dieRollString {
  97. // Check for forbidden characters.
  98. if ([dieRollString containsCharactersInSet:[[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidCharacters]] invertedSet]]) {
  99. SA_DiceExpression *errorExpression = [SA_DiceExpression new];
  100. errorExpression.type = SA_DiceExpressionTerm_NONE;
  101. errorExpression.inputString = dieRollString;
  102. errorExpression.errorBitMask |= SA_DiceExpressionError_ROLL_STRING_HAS_ILLEGAL_CHARACTERS;
  103. return errorExpression;
  104. }
  105. // Since we have checked the entire string for forbidden characters, we can
  106. // now begin parsing the string; there is no need to check substrings for
  107. // illegal characters (which is why we do it only once, in this wrapper
  108. // method). When constructing the expression tree, we call
  109. // legacyExpressionForLegalString:, not legacyExpressionForString:, when
  110. // recursively parsing substrings.
  111. return [self legacyExpressionForLegalString:dieRollString];
  112. }
  113. -(SA_DiceExpression *) legacyExpressionForLegalString:(NSString *)dieRollString {
  114. // Make sure string is not empty.
  115. if (dieRollString.length == 0) {
  116. SA_DiceExpression *errorExpression = [SA_DiceExpression new];
  117. errorExpression.type = SA_DiceExpressionTerm_NONE;
  118. errorExpression.inputString = dieRollString;
  119. errorExpression.errorBitMask |= SA_DiceExpressionError_ROLL_STRING_EMPTY;
  120. return errorExpression;
  121. }
  122. // We now know the string describes one of the allowable expression types
  123. // (probably; it could be malformed in some way other than being empty or
  124. // containing forbidden characters, such as e.g. by starting with a + sign).
  125. // Check to see if the top-level term is an operation. Note that we parse
  126. // operator expressions left-associatively.
  127. NSRange lastOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet
  128. characterSetWithCharactersInString:[SA_DiceParser
  129. allValidOperatorCharacters]]
  130. options:NSBackwardsSearch];
  131. if (lastOperatorRange.location != NSNotFound) {
  132. NSString *operator = [dieRollString substringWithRange:lastOperatorRange];
  133. if (lastOperatorRange.location != 0) {
  134. return [self legacyExpressionForStringDescribingOperation:dieRollString
  135. withOperatorString:operator
  136. atRange:lastOperatorRange];
  137. } else {
  138. // If the last (and thus only) operator is the leading character of
  139. // the expression, then this is one of several possible special cases.
  140. // First, we check for whether there even is anything more to the
  141. // roll string besides the operator. If not, then the string is
  142. // malformed by definition...
  143. // If the last operator is the leading character (i.e. there’s just
  144. // one operator in the expression, and it’s at the beginning), and
  145. // there’s more to the expression than just the operator, then
  146. // this is either an expression whose first term (which may or may
  147. // not be its only term) is a simple value expression which
  148. // represents a negative number - or, it’s a malformed expression
  149. // (because operators other than negation cannot begin an
  150. // expression).
  151. // In the former case, we do nothing, letting the testing for
  152. // expression type fall through to the remaining cases (roll command
  153. // or simple value).
  154. // In the latter case, we register an error and return.
  155. if ( dieRollString.length == lastOperatorRange.length
  156. || ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] containsCharactersInString:operator] == NO)) {
  157. SA_DiceExpression *expression = [SA_DiceExpression new];
  158. expression.type = SA_DiceExpressionTerm_OPERATION;
  159. expression.inputString = dieRollString;
  160. expression.errorBitMask |= SA_DiceExpressionError_INVALID_EXPRESSION;
  161. return expression;
  162. }
  163. // We’ve determined that this expression begins with a simple
  164. // value expression that represents a negative number.
  165. // This next line is a hack to account for the fact that Cocoa’s
  166. // Unicode compliance is incomplete. :( NSString’s integerValue
  167. // method only accepts the hyphen as a negation sign when reading a
  168. // number - not any of the Unicode characters which officially
  169. // symbolize negation! But we are more modern-minded, and accept
  170. // arbitrary symbols as minus-sign. For proper parsing, though,
  171. // we have to replace it like this...
  172. dieRollString = [dieRollString stringByReplacingCharactersInRange:lastOperatorRange
  173. withString:@"-"];
  174. // Now we fall through to “is it a roll command, or maybe a simple
  175. // value?”...
  176. }
  177. }
  178. // If not an operation, the top-level term might be a die roll command
  179. // or a die roll modifier.
  180. // Look for one of the characters recognized as valid die roll or die roll
  181. // modifier delimiters.
  182. // Note that we parse roll commands left-associatively, therefore e.g.
  183. // 5d6d10 parses as “roll N d10s, where N is the result of rolling 5d6”.
  184. NSMutableCharacterSet *validDelimiterCharacters = [NSMutableCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidRollCommandDelimiterCharacters]];
  185. [validDelimiterCharacters addCharactersInString:[SA_DiceParser allValidRollModifierDelimiterCharacters]];
  186. NSRange lastDelimiterRange = [dieRollString rangeOfCharacterFromSet:validDelimiterCharacters
  187. options:NSBackwardsSearch];
  188. if (lastDelimiterRange.location != NSNotFound) {
  189. if ([[SA_DiceParser allValidRollCommandDelimiterCharacters] containsString:[dieRollString substringWithRange:lastDelimiterRange]])
  190. return [self legacyExpressionForStringDescribingRollCommand:dieRollString
  191. withDelimiterAtRange:lastDelimiterRange];
  192. else if ([[SA_DiceParser allValidRollModifierDelimiterCharacters] containsString:[dieRollString substringWithRange:lastDelimiterRange]])
  193. return [self legacyExpressionForStringDescribingRollModifier:dieRollString
  194. withDelimiterAtRange:lastDelimiterRange];
  195. else
  196. // This should be impossible.
  197. NSLog(@"IMPOSSIBLE CONDITION ENCOUNTERED WHILE PARSING DIE ROLL STRING!");
  198. }
  199. // If not an operation nor a roll command, the top-level term can only be
  200. // a simple numeric value.
  201. return [self legacyExpressionForStringDescribingNumericValue:dieRollString];
  202. }
  203. -(SA_DiceExpression *) legacyExpressionForStringDescribingOperation:(NSString *)dieRollString
  204. withOperatorString:(NSString *)operatorString
  205. atRange:(NSRange)operatorRange {
  206. SA_DiceExpression *expression = [SA_DiceExpression new];
  207. expression.type = SA_DiceExpressionTerm_OPERATION;
  208. expression.inputString = dieRollString;
  209. // Operands of a binary operator are the expressions generated by
  210. // parsing the strings before and after the addition operator.
  211. expression.leftOperand = [self legacyExpressionForLegalString:[dieRollString substringToIndex:operatorRange.location]];
  212. expression.rightOperand = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(operatorRange.location + operatorRange.length)]];
  213. if ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_PLUS] containsCharactersInString:operatorString]) {
  214. // Check to see if the term is an addition operation.
  215. expression.operator = SA_DiceExpressionOperator_PLUS;
  216. } else if ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] containsCharactersInString:operatorString]) {
  217. // Check to see if the term is a subtraction operation.
  218. expression.operator = SA_DiceExpressionOperator_MINUS;
  219. } else if ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_TIMES] containsCharactersInString:operatorString]) {
  220. // Check to see if the term is a multiplication operation.
  221. // Look for other, lower-precedence operators to the left of the
  222. // multiplication operator. If found, split the string there
  223. // instead of at the current operator.
  224. NSString *allLowerPrecedenceOperators = [@[ [SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_PLUS],
  225. [SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] ]
  226. componentsJoinedByString:@""];
  227. NSRange lastLowerPrecedenceOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet
  228. characterSetWithCharactersInString:allLowerPrecedenceOperators]
  229. options:NSBackwardsSearch
  230. range:NSRangeMake(1, operatorRange.location - 1)];
  231. if (lastLowerPrecedenceOperatorRange.location != NSNotFound) {
  232. return [self legacyExpressionForStringDescribingOperation:dieRollString
  233. withOperatorString:[dieRollString substringWithRange:lastLowerPrecedenceOperatorRange]
  234. atRange:lastLowerPrecedenceOperatorRange];
  235. }
  236. expression.operator = SA_DiceExpressionOperator_TIMES;
  237. } else {
  238. expression.errorBitMask |= SA_DiceExpressionError_UNKNOWN_OPERATOR;
  239. }
  240. // The operands have now been parsed recursively; this parsing may have
  241. // generated one or more errors. Inherit any error(s) from the
  242. // error-generating operand(s).
  243. expression.errorBitMask |= expression.leftOperand.errorBitMask;
  244. expression.errorBitMask |= expression.rightOperand.errorBitMask;
  245. return expression;
  246. }
  247. -(SA_DiceExpression *) legacyExpressionForStringDescribingRollCommand:(NSString *)dieRollString
  248. withDelimiterAtRange:(NSRange)delimiterRange {
  249. SA_DiceExpression *expression = [SA_DiceExpression new];
  250. expression.type = SA_DiceExpressionTerm_ROLL_COMMAND;
  251. expression.inputString = dieRollString;
  252. // For now, only two kinds of roll command is supported - roll-and-sum,
  253. // and roll-and-sum with exploding dice.
  254. // These roll one or more dice of a given sort, and determine the sum of
  255. // their rolled values. (In the “exploding dice” version, each die can
  256. // explode, of course.)
  257. if ([[SA_DiceParser validCharactersForRollCommandDelimiter:SA_DiceExpressionRollCommand_SUM]
  258. containsString:[dieRollString substringWithRange:delimiterRange]])
  259. expression.rollCommand = SA_DiceExpressionRollCommand_SUM;
  260. else if ([[SA_DiceParser validCharactersForRollCommandDelimiter:SA_DiceExpressionRollCommand_SUM_EXPLODING]
  261. containsString:[dieRollString substringWithRange:delimiterRange]])
  262. expression.rollCommand = SA_DiceExpressionRollCommand_SUM_EXPLODING;
  263. // Check to see if the delimiter is the initial character of the roll
  264. // string. If so (i.e. if the die count is omitted), we assume it to be 1
  265. // (i.e. ‘d6’ is read as ‘1d6’).
  266. // Otherwise, the die count is the expression generated by parsing the
  267. // string before the delimiter.
  268. expression.dieCount = ((delimiterRange.location == 0) ?
  269. [self legacyExpressionForStringDescribingNumericValue:@"1"] :
  270. [self legacyExpressionForLegalString:[dieRollString substringToIndex:delimiterRange.location]]);
  271. // The die size is the expression generated by parsing the string after the
  272. // delimiter.
  273. expression.dieSize = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(delimiterRange.location + delimiterRange.length)]];
  274. if ([expression.dieSize.inputString.lowercaseString isEqualToString:@"f"])
  275. expression.dieType = SA_DiceExpressionDice_FUDGE;
  276. // The die count and die size have now been parsed recursively; this parsing
  277. // may have generated one or more errors. Inherit any error(s) from the
  278. // error-generating sub-terms.
  279. expression.errorBitMask |= expression.dieCount.errorBitMask;
  280. expression.errorBitMask |= expression.dieSize.errorBitMask;
  281. return expression;
  282. }
  283. -(SA_DiceExpression *) legacyExpressionForStringDescribingRollModifier:(NSString *)dieRollString
  284. withDelimiterAtRange:(NSRange)delimiterRange {
  285. SA_DiceExpression *expression = [SA_DiceExpression new];
  286. expression.type = SA_DiceExpressionTerm_ROLL_MODIFIER;
  287. expression.inputString = dieRollString;
  288. // The possible roll modifiers are KEEP HIGHEST and KEEP LOWEST.
  289. // These take a roll command and a number, and keep that number of rolls
  290. // generated by the roll command (either the highest or lowest rolls,
  291. // respectively).
  292. if ([[SA_DiceParser validCharactersForRollModifierDelimiter:SA_DiceExpressionRollModifier_KEEP_HIGHEST]
  293. containsString:[dieRollString substringWithRange:delimiterRange]])
  294. expression.rollModifier = SA_DiceExpressionRollModifier_KEEP_HIGHEST;
  295. else if ([[SA_DiceParser validCharactersForRollModifierDelimiter:SA_DiceExpressionRollModifier_KEEP_LOWEST]
  296. containsString:[dieRollString substringWithRange:delimiterRange]])
  297. expression.rollModifier = SA_DiceExpressionRollModifier_KEEP_LOWEST;
  298. // Check to see if the delimiter is the initial character of the roll
  299. // string. If so, set an error, because a roll modifier requires a
  300. // roll command to modify.
  301. if (delimiterRange.location == 0) {
  302. expression.errorBitMask |= SA_DiceExpressionError_ROLL_STRING_EMPTY;
  303. return expression;
  304. }
  305. // Otherwise, the left operand is the expression generated by parsing the
  306. // string before the delimiter.
  307. expression.leftOperand = [self legacyExpressionForLegalString:[dieRollString substringToIndex:delimiterRange.location]];
  308. // The right operand is the expression generated by parsing the string after
  309. // the delimiter.
  310. expression.rightOperand = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(delimiterRange.location + delimiterRange.length)]];
  311. // The left and right operands have now been parsed recursively; this
  312. // parsing may have generated one or more errors. Inherit any error(s) from
  313. // the error-generating sub-terms.
  314. expression.errorBitMask |= expression.leftOperand.errorBitMask;
  315. expression.errorBitMask |= expression.rightOperand.errorBitMask;
  316. return expression;
  317. }
  318. -(SA_DiceExpression *) legacyExpressionForStringDescribingNumericValue:(NSString *)dieRollString {
  319. SA_DiceExpression *expression = [SA_DiceExpression new];
  320. expression.type = SA_DiceExpressionTerm_VALUE;
  321. expression.inputString = dieRollString;
  322. if ([expression.inputString.lowercaseString isEqualToString:@"f"])
  323. expression.value = @(-1);
  324. else
  325. expression.value = @(dieRollString.integerValue);
  326. return expression;
  327. }
  328. /****************************/
  329. #pragma mark - Helper methods
  330. /****************************/
  331. +(void) loadValidCharactersDict {
  332. NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME
  333. ofType:@"plist"];
  334. _validCharactersDict = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath][SA_DB_VALID_CHARACTERS];
  335. if (!_validCharactersDict) {
  336. NSLog(@"Could not load valid characters dictionary!");
  337. }
  338. }
  339. // TODO: Should this be on a per-mode, and therefore per-instance, basis?
  340. +(NSString *) allValidCharacters {
  341. return [ @[ [SA_DiceParser validNumeralCharacters],
  342. [SA_DiceParser allValidRollCommandDelimiterCharacters],
  343. [SA_DiceParser allValidRollModifierDelimiterCharacters],
  344. [SA_DiceParser allValidOperatorCharacters] ] componentsJoinedByString:@""];
  345. }
  346. +(NSString *) allValidOperatorCharacters {
  347. NSDictionary *validOperatorCharactersDict = [SA_DiceParser validCharactersDict][SA_DB_VALID_OPERATOR_CHARACTERS];
  348. return [validOperatorCharactersDict.allValues componentsJoinedByString:@""];
  349. }
  350. +(NSString *) validCharactersForOperator:(SA_DiceExpressionOperator)operator {
  351. return [SA_DiceParser validCharactersDict][SA_DB_VALID_OPERATOR_CHARACTERS][NSStringFromSA_DiceExpressionOperator(operator)];
  352. }
  353. +(NSString *) validNumeralCharacters {
  354. return [SA_DiceParser validCharactersDict][SA_DB_VALID_NUMERAL_CHARACTERS];
  355. }
  356. +(NSString *) validCharactersForRollCommandDelimiter:(SA_DiceExpressionRollCommand)command {
  357. return [SA_DiceParser validCharactersDict][SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS][NSStringFromSA_DiceExpressionRollCommand(command)];
  358. }
  359. +(NSString *) allValidRollCommandDelimiterCharacters {
  360. NSDictionary *validRollCommandDelimiterCharactersDict = [SA_DiceParser validCharactersDict][SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS];
  361. return [validRollCommandDelimiterCharactersDict.allValues componentsJoinedByString:@""];
  362. }
  363. +(NSString *) validCharactersForRollModifierDelimiter:(SA_DiceExpressionRollModifier)modifier {
  364. return [SA_DiceParser validCharactersDict][SA_DB_VALID_ROLL_MODIFIER_DELIMITER_CHARACTERS][NSStringFromSA_DiceExpressionRollModifier(modifier)];
  365. }
  366. +(NSString *) allValidRollModifierDelimiterCharacters {
  367. NSDictionary *validRollModifierDelimiterCharactersDict = [SA_DiceParser validCharactersDict][SA_DB_VALID_ROLL_MODIFIER_DELIMITER_CHARACTERS];
  368. return [validRollModifierDelimiterCharactersDict.allValues componentsJoinedByString:@""];
  369. }
  370. @end