Updated LICENSE and info comments
[SA_Dice.git] / SA_DiceParser.m
blobd48cd2350e752d154f31e246f5cd946ee81a537a
1 //
2 //  SA_DiceParser.m
3 //
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;
35         
36         switch (_parserBehavior) {
37                 case SA_DiceParserBehaviorLegacy:
38                 case SA_DiceParserBehaviorModern:
39                 case SA_DiceParserBehaviorFeepbot:
40                         break;
41                         
42                 case SA_DiceParserBehaviorDefault:
43                 default:
44                         _parserBehavior = SA_DiceParser.defaultParserBehavior;
45                         break;
46         }
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;
60         } else {
61                 _defaultParserBehavior = newDefaultParserBehavior;
62         }
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];
73         }
74         
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]))
88                 return nil;
90         self.parserBehavior = parserBehavior;
92         if (_validCharactersDict == nil) {
93                 [SA_DiceParser loadValidCharactersDict];
94         }
96         return self;
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];
114         } else {
115                 return nil;
116         }
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;
131         }
132         
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;
150         }
151         
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];
164                 
165                 if (lastOperatorRange.location != 0) {
166                         return [self legacyExpressionForStringDescribingOperation:dieRollString
167                                                                                                    withOperatorString:operator
168                                                                                                                           atRange:lastOperatorRange];
169                 } else {
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
182                         // expression).
183                         // In the former case, we do nothing, letting the testing for
184                         // expression type fall through to the remaining cases (roll command
185                         // or simple value).
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;
193                                 return expression;
194                         }
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
206                                                                                                                                    withString:@"-"];
208                         // Now we fall through to “is it a roll command, or maybe a simple
209                         // value?”...
210                 }
211         }
212         
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];
230                 else
231                         // This should be impossible.
232                         NSLog(@"IMPOSSIBLE CONDITION ENCOUNTERED WHILE PARSING DIE ROLL STRING!");
233         }
234         
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)]];
252         
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];
275                 }
276                 
277                 expression.operator = SA_DiceExpressionOperator_TIMES;
278         } else {
279                 expression.errorBitMask |= SA_DiceExpressionError_UNKNOWN_OPERATOR;
280         }
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;
288         return expression;
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 
320         // delimiter.
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;
331         return expression;
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,
344         // respectively).
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;
357                 return expression;
358         }
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
365         // the delimiter.
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;
374         return expression;
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);
384         else
385                 expression.value = @(dieRollString.integerValue);
387         return expression;
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
396                                                                                                                                                                            ofType:@"plist"];
397         _validCharactersDict = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath][SA_DB_VALID_CHARACTERS];
398         if (!_validCharactersDict) {
399                 NSLog(@"Could not load valid characters dictionary!");
400         }
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];
413         
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:@""];
445 @end