A set of classes for parsing, evaluating, and formatting die roll strings.
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

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