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 19KB

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