| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- //
- // SA_DiceParser.m
- //
- // Copyright (c) 2016 Said Achmiz.
- //
- // This software is licensed under the MIT license.
- // See the file "LICENSE" for more information.
-
- #import "SA_DiceParser.h"
-
- #import "SA_DiceExpressionStringConstants.h"
- #import "SA_DiceErrorHandling.h"
- #import "NSString+SA_NSStringExtensions.h"
-
- /********************************/
- #pragma mark File-scope variables
- /********************************/
-
- static SA_DiceParserBehavior _defaultParserBehavior = SA_DiceParserBehaviorLegacy;
- static NSDictionary *_validCharactersDict;
-
- /************************************************/
- #pragma mark - SA_DiceParser class implementation
- /************************************************/
-
- @implementation SA_DiceParser
- {
- SA_DiceParserBehavior _parserBehavior;
- }
-
- /************************/
- #pragma mark - Properties
- /************************/
-
- - (void)setParserBehavior:(SA_DiceParserBehavior)newParserBehavior
- {
- _parserBehavior = newParserBehavior;
-
- switch (_parserBehavior)
- {
- case SA_DiceParserBehaviorLegacy:
- case SA_DiceParserBehaviorModern:
- case SA_DiceParserBehaviorFeepbot:
- break;
-
- case SA_DiceParserBehaviorDefault:
- default:
- _parserBehavior = [SA_DiceParser defaultParserBehavior];
- break;
- }
- }
-
- - (SA_DiceParserBehavior)parserBehavior
- {
- return _parserBehavior;
- }
-
- /****************************************/
- #pragma mark - "Class property" accessors
- /****************************************/
-
- + (void)setDefaultParserBehavior:(SA_DiceParserBehavior)newDefaultParserBehavior
- {
- if(newDefaultParserBehavior == SA_DiceParserBehaviorDefault)
- {
- _defaultParserBehavior = SA_DiceParserBehaviorLegacy;
- }
- else
- {
- _defaultParserBehavior = newDefaultParserBehavior;
- }
- }
-
- + (SA_DiceParserBehavior)defaultParserBehavior
- {
- return _defaultParserBehavior;
- }
-
- + (NSDictionary *)validCharactersDict
- {
- if(_validCharactersDict == nil)
- {
- [SA_DiceParser loadValidCharactersDict];
- }
-
- return _validCharactersDict;
- }
-
- /********************************************/
- #pragma mark - Initializers & factory methods
- /********************************************/
-
- - (instancetype)init
- {
- return [self initWithBehavior:SA_DiceParserBehaviorDefault];
- }
-
- - (instancetype)initWithBehavior:(SA_DiceParserBehavior)parserBehavior
- {
- if(self = [super init])
- {
- self.parserBehavior = parserBehavior;
-
- if(_validCharactersDict == nil)
- {
- [SA_DiceParser loadValidCharactersDict];
- }
- }
- return self;
- }
-
- + (instancetype)defaultParser
- {
- return [[SA_DiceParser alloc] initWithBehavior:SA_DiceParserBehaviorDefault];
- }
-
- + (instancetype)parserWithBehavior:(SA_DiceParserBehavior)parserBehavior
- {
- return [[SA_DiceParser alloc] initWithBehavior:parserBehavior];
- }
-
- /****************************/
- #pragma mark - Public methods
- /****************************/
-
- - (NSDictionary *)expressionForString:(NSString *)dieRollString
- {
- if(_parserBehavior == SA_DiceParserBehaviorLegacy)
- {
- return [self legacyExpressionForString:dieRollString];
- }
- else
- {
- return @{};
- }
- }
-
- /**********************************************/
- #pragma mark - "Legacy" behavior implementation
- /**********************************************/
-
- - (NSDictionary *)legacyExpressionForString:(NSString *)dieRollString
- {
- // Check for forbidden characters.
- NSCharacterSet *forbiddenCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidCharacters]] invertedSet];
- if([dieRollString containsCharactersInSet:forbiddenCharacterSet])
- {
- return @{ SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_NONE,
- SA_DB_INPUT_STRING : dieRollString,
- SA_DB_ERRORS : @[SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS] };
- }
-
- // Since we have checked the entire string for forbidden characters, we can
- // now begin parsing the string; there is no need to check substrings for
- // illegal characters (which is why we do it only once, in this wrapper
- // method). When constructing the expression tree, we call
- // legacyExpressionForLegalString:, not legacyExpressionForString:, when
- // recursively parsing substrings.
- return [self legacyExpressionForLegalString:dieRollString];
- }
-
- - (NSDictionary *)legacyExpressionForLegalString:(NSString *)dieRollString
- {
- // Make sure string is not empty.
- if(dieRollString.length == 0)
- {
- return @{ SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_NONE,
- SA_DB_INPUT_STRING : dieRollString,
- SA_DB_ERRORS : @[SA_DB_ERROR_ROLL_STRING_EMPTY] };
- }
-
- // We now know the string describes one of the allowable expression types
- // (probably; it could be malformed in some way other than being empty or
- // containing forbidden characters, such as e.g. by starting with a + sign).
-
- // Check to see if the top-level term is an operation. Note that we parse
- // operator expressions left-associatively.
- NSRange lastOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidOperatorCharacters]] options:NSBackwardsSearch];
- if(lastOperatorRange.location != NSNotFound)
- {
- NSString *operator = [dieRollString substringWithRange:lastOperatorRange];
-
- return [self legacyExpressionForStringDescribingOperation:dieRollString withOperator:operator atRange:lastOperatorRange];
- }
-
- // If not an operation, the top-level term might be a die roll command.
- // Look for one of the characters recognized as valid die roll delimiters.
- // Note that we parse roll commands left-associatively, therefore e.g.
- // 5d6d10 parses as "roll N d10s, where N is the result of rolling 5d6".
- NSRange lastRollCommandDelimiterRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser validRollCommandDelimiterCharacters]] options:NSBackwardsSearch];
- if(lastRollCommandDelimiterRange.location != NSNotFound)
- {
- return [self legacyExpressionForStringDescribingRollCommand:dieRollString withDelimiterAtRange:lastRollCommandDelimiterRange];
- }
-
- // If not an operation nor a roll command, the top-level term can only be
- // a simple numeric value.
- return [self legacyExpressionForStringDescribingNumericValue:dieRollString];
- }
-
- - (NSDictionary *)legacyExpressionForStringDescribingOperation:(NSString *)dieRollString withOperator:(NSString *)operator atRange:(NSRange)operatorRange
- {
- NSMutableDictionary *expression;
-
- // If the operator is at the beginning, this may be negation; we handle that
- // case later below. Otherwise, it's a binary operator.
- if(operatorRange.location != 0)
- {
- expression = [NSMutableDictionary dictionary];
- expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
- expression[SA_DB_INPUT_STRING] = dieRollString;
-
- // Operands of a binary operator are the expressions generated by
- // parsing the strings before and after the addition operator.
- expression[SA_DB_OPERAND_LEFT] = [self legacyExpressionForLegalString:[dieRollString substringToIndex:operatorRange.location]];
- expression[SA_DB_OPERAND_RIGHT] = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(operatorRange.location + operatorRange.length)]];
-
- // Check to see if the term is a subtraction operation.
- if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS] containsCharactersInString:operator])
- {
- expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_MINUS;
- }
- // Check to see if the term is an addition operation.
- else if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_PLUS] containsCharactersInString:operator])
- {
- expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_PLUS;
- }
- // Check to see if the term is a multiplication operation.
- else if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_TIMES] containsCharactersInString:operator])
- {
- // Look for other, lower-precedence operators to the left of the
- // multiplication operator. If found, split the string there
- // instead of at the current operator.
- NSString *allLowerPrecedenceOperators = [NSString stringWithFormat:@"%@%@", [SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_PLUS], [SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS]];
- NSRange lastLowerPrecedenceOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:allLowerPrecedenceOperators] options:NSBackwardsSearch range:NSMakeRange(1, operatorRange.location - 1)];
- if(lastLowerPrecedenceOperatorRange.location != NSNotFound)
- {
- NSString *lowerPrecedenceOperator = [dieRollString substringWithRange:lastLowerPrecedenceOperatorRange];
- return [self legacyExpressionForStringDescribingOperation:dieRollString withOperator:lowerPrecedenceOperator atRange:lastLowerPrecedenceOperatorRange];
- }
-
- expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_TIMES;
- }
- else
- {
- addErrorToExpression(SA_DB_ERROR_UNKNOWN_OPERATOR, expression);
- }
- }
- // Check to see if the term is a negation operation (we can only reach this
- // case if the subtraction operator is at the beginning).
- else
- {
- if(dieRollString.length == operatorRange.length)
- {
- expression = [NSMutableDictionary dictionary];
- expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
- expression[SA_DB_INPUT_STRING] = dieRollString;
-
- addErrorToExpression(SA_DB_ERROR_INVALID_EXPRESSION, expression);
-
- return expression;
- }
- if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS] containsCharactersInString:operator])
- {
- // It turns out this isn't actually an operation expression. It's a
- // simple value expression with a negative number.
-
- // This next line is a hack to account for the fact that Cocoa's
- // Unicode compliance is incomplete. :( NSString's integerValue
- // method only accepts the hyphen as a negation sign when reading a
- // number - not any of the Unicode characters which officially
- // symbolize negation! But we are more modern-minded, and accept
- // arbitrary symbols as minus-sign. For proper parsing, though,
- // we have to replace it like this...
- NSString *fixedRollString = [dieRollString stringByReplacingCharactersInRange:operatorRange withString:@"-"];
-
- return [self legacyExpressionForStringDescribingNumericValue:fixedRollString];
- }
- else
- {
- expression = [NSMutableDictionary dictionary];
- expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION;
- expression[SA_DB_INPUT_STRING] = dieRollString;
-
- addErrorToExpression(SA_DB_ERROR_INVALID_EXPRESSION, expression);
-
- return expression;
- }
- }
-
- // The operands have now been parsed recursively; this parsing may have
- // generated one or more errors. Inherit any error(s) from the
- // error-generating operand(s).
- addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_RIGHT], expression);
- addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_LEFT], expression);
-
- return expression;
- }
-
- - (NSDictionary *)legacyExpressionForStringDescribingRollCommand:(NSString *)dieRollString withDelimiterAtRange:(NSRange)delimiterRange
- {
- NSMutableDictionary *expression = [NSMutableDictionary dictionary];
- expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_ROLL_COMMAND;
- expression[SA_DB_INPUT_STRING] = dieRollString;
-
- // For now, only one kind of roll command is supported - roll-and-sum.
- // This rolls one or more dice of a given sort, and determines the sum of
- // their rolled values.
- // In the future, support for other, more complex roll commands might be
- // added, such as "roll several and return the highest", exploding dice,
- // etc.
- expression[SA_DB_ROLL_COMMAND] = SA_DB_ROLL_COMMAND_SUM;
-
- // Check to see if the delimiter is the initial character of the roll
- // string. If so (i.e. if the die count is omitted), we assume it to be 1
- // (i.e. 'd6' is read as '1d6').
- if(delimiterRange.location == 0)
- {
- expression[SA_DB_ROLL_DIE_COUNT] = [self legacyExpressionForStringDescribingNumericValue:@"1"];
- }
- else
- {
- // The die count is the expression generated by parsing the string
- // before the delimiter.
- expression[SA_DB_ROLL_DIE_COUNT] = [self legacyExpressionForLegalString:[dieRollString substringToIndex:delimiterRange.location]];
- }
-
- // The die size is the expression generated by parsing the string after the
- // delimiter.
- expression[SA_DB_ROLL_DIE_SIZE] = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(delimiterRange.location + delimiterRange.length)]];
-
- // The die count and die size have now been parsed recursively; this parsing
- // may have generated one or more errors. Inherit any error(s) from the
- // error-generating sub-terms.
- addErrorsFromExpressionToExpression(expression[SA_DB_ROLL_DIE_COUNT], expression);
- addErrorsFromExpressionToExpression(expression[SA_DB_ROLL_DIE_SIZE], expression);
-
- return expression;
- }
-
- - (NSDictionary *)legacyExpressionForStringDescribingNumericValue:(NSString *)dieRollString
- {
- NSMutableDictionary *expression = [NSMutableDictionary dictionary];
- expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_VALUE;
- expression[SA_DB_INPUT_STRING] = dieRollString;
-
- expression[SA_DB_VALUE] = @(dieRollString.integerValue);
-
- return expression;
- }
-
- /****************************/
- #pragma mark - Helper methods
- /****************************/
-
- + (void)loadValidCharactersDict
- {
- NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME ofType:@"plist"];
- _validCharactersDict = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath][SA_DB_VALID_CHARACTERS];
- if(!_validCharactersDict)
- {
- NSLog(@"Could not load valid characters dictionary!");
- }
- }
-
- + (NSString *)allValidCharacters
- {
- NSMutableString *validCharactersString = [NSMutableString string];
-
- [validCharactersString appendString:[SA_DiceParser validNumeralCharacters]];
- [validCharactersString appendString:[SA_DiceParser validRollCommandDelimiterCharacters]];
- [validCharactersString appendString:[SA_DiceParser allValidOperatorCharacters]];
-
- return validCharactersString;
- }
-
- + (NSString *)allValidOperatorCharacters
- {
- NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
-
- __block NSMutableString *validOperatorCharactersString = [NSMutableString string];
-
- [validCharactersDict[SA_DB_VALID_OPERATOR_CHARACTERS] enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) {
- [validOperatorCharactersString appendString:value];
- }];
-
- return validOperatorCharactersString;
- }
-
- + (NSString *)validCharactersForOperator:(NSString *)operatorName
- {
- return [SA_DiceParser validCharactersDict][SA_DB_VALID_OPERATOR_CHARACTERS][operatorName];
- }
-
- + (NSString *)validNumeralCharacters
- {
- NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
-
- return validCharactersDict[SA_DB_VALID_NUMERAL_CHARACTERS];
- }
-
- + (NSString *)validRollCommandDelimiterCharacters
- {
- NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict];
-
- return validCharactersDict[SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS];
- }
-
- @end
|