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

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