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