A set of classes for parsing, evaluating, and formatting die roll strings.
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

SA_DiceParser.m 19KB

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