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

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