| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- //
- // 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_DiceFormatter.h"
-
- #import "SA_Utility.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 properties
- /******************************/
-
- +(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
- /****************************/
-
- -(SA_DiceExpression *) expressionForString:(NSString *)dieRollString {
- if (_parserBehavior == SA_DiceParserBehaviorLegacy) {
- return [self legacyExpressionForString:dieRollString];
- } else {
- return nil;
- }
- }
-
- -(SA_DiceExpression *) expressionByJoiningExpression:(SA_DiceExpression *)leftHandExpression
- toExpression:(SA_DiceExpression *)rightHandExpression
- withOperator:(SA_DiceExpressionOperator)operator {
- SA_DiceExpression *expression = [SA_DiceExpression new];
-
- // First, we check that the operands and operator are not nil. If they are,
- // then the expression is invalid...
- if (leftHandExpression == nil ||
- rightHandExpression == nil ||
- operator == SA_DiceExpressionOperator_NONE) {
- expression.type = SA_DiceExpressionTerm_NONE;
- expression.errorBitMask |= SA_DiceExpressionError_INVALID_EXPRESSION;
- return expression;
- }
-
- // If the operands and operator are present, then the expression is an
- // operation expression...
- expression.type = SA_DiceExpressionTerm_OPERATION;
-
- // ... but does it have a valid operator?
- if (operator == SA_DiceExpressionOperator_MINUS ||
- operator == SA_DiceExpressionOperator_PLUS ||
- operator == SA_DiceExpressionOperator_TIMES) {
- expression.operator = operator;
- } else {
- expression.errorBitMask |= SA_DiceExpressionError_UNKNOWN_OPERATOR;
- return expression;
- }
-
- // The operator is valid. Set the operands...
- expression.leftOperand = leftHandExpression;
- expression.rightOperand = rightHandExpression;
-
- // And inherit any errors that they may have.
- expression.errorBitMask |= expression.leftOperand.errorBitMask;
- expression.errorBitMask |= expression.rightOperand.errorBitMask;
-
- // Since this top-level expression was NOT generated by parsing an input
- // string, for completeness and consistency, we have to generate a fake
- // input string ourselves! We do this by wrapping each operand in
- // parentheses and putting the canonical representation of the operator
- // between them.
- expression.inputString = [NSString stringWithFormat:@"(%@)%@(%@)",
- expression.leftOperand.inputString,
- [SA_DiceFormatter canonicalRepresentationForOperator:expression.operator],
- expression.rightOperand.inputString];
-
- // The joining is complete. (Power overwhelming.)
- return expression;
- }
-
- /**********************************************/
- #pragma mark - “Legacy” behavior implementation
- /**********************************************/
-
- -(SA_DiceExpression *) legacyExpressionForString:(NSString *)dieRollString {
- // Check for forbidden characters.
- if ([dieRollString containsCharactersInSet:[[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidCharacters]] invertedSet]]) {
- SA_DiceExpression *errorExpression = [SA_DiceExpression new];
- errorExpression.type = SA_DiceExpressionTerm_NONE;
- errorExpression.inputString = dieRollString;
- errorExpression.errorBitMask |= SA_DiceExpressionError_ROLL_STRING_HAS_ILLEGAL_CHARACTERS;
- return errorExpression;
- }
-
- // 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];
- }
-
- -(SA_DiceExpression *) legacyExpressionForLegalString:(NSString *)dieRollString {
- // Make sure string is not empty.
- if (dieRollString.length == 0) {
- SA_DiceExpression *errorExpression = [SA_DiceExpression new];
- errorExpression.type = SA_DiceExpressionTerm_NONE;
- errorExpression.inputString = dieRollString;
- errorExpression.errorBitMask |= SA_DiceExpressionError_ROLL_STRING_EMPTY;
- return errorExpression;
- }
-
- // 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];
-
- if (lastOperatorRange.location != 0) {
- return [self legacyExpressionForStringDescribingOperation:dieRollString
- withOperatorString:operator
- atRange:lastOperatorRange];
- } else {
- // If the last (and thus only) operator is the leading character of
- // the expression, then this is one of several possible special cases.
- // First, we check for whether there even is anything more to the
- // roll string besides the operator. If not, then the string is
- // malformed by definition...
- // If the last operator is the leading character (i.e. there’s just
- // one operator in the expression, and it’s at the beginning), and
- // there’s more to the expression than just the operator, then
- // this is either an expression whose first term (which may or may
- // not be its only term) is a simple value expression which
- // represents a negative number - or, it’s a malformed expression
- // (because operators other than negation cannot begin an
- // expression).
- // In the former case, we do nothing, letting the testing for
- // expression type fall through to the remaining cases (roll command
- // or simple value).
- // In the latter case, we register an error and return.
- if (dieRollString.length == lastOperatorRange.length ||
- ![[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] containsCharactersInString:operator]) {
- SA_DiceExpression *expression = [SA_DiceExpression new];
-
- expression.type = SA_DiceExpressionTerm_OPERATION;
- expression.inputString = dieRollString;
- expression.errorBitMask |= SA_DiceExpressionError_INVALID_EXPRESSION;
- return expression;
- }
-
- // We’ve determined that this expression begins with a simple
- // value expression that represents 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...
- dieRollString = [dieRollString stringByReplacingCharactersInRange:lastOperatorRange
- withString:@"-"];
-
- // Now we fall through to “is it a roll command, or maybe a simple
- // value?”...
- }
- }
-
- // If not an operation, the top-level term might be a die roll command
- // or a die roll modifier.
- // Look for one of the characters recognized as valid die roll or die roll
- // modifier 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”.
- NSMutableCharacterSet *validDelimiterCharacters = [NSMutableCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidRollCommandDelimiterCharacters]];
- [validDelimiterCharacters addCharactersInString:[SA_DiceParser allValidRollModifierDelimiterCharacters]];
- NSRange lastDelimiterRange = [dieRollString rangeOfCharacterFromSet:validDelimiterCharacters
- options:NSBackwardsSearch];
- if (lastDelimiterRange.location != NSNotFound) {
- if ([[SA_DiceParser allValidRollCommandDelimiterCharacters] containsString:[dieRollString substringWithRange:lastDelimiterRange]])
- return [self legacyExpressionForStringDescribingRollCommand:dieRollString
- withDelimiterAtRange:lastDelimiterRange];
- else if ([[SA_DiceParser allValidRollModifierDelimiterCharacters] containsString:[dieRollString substringWithRange:lastDelimiterRange]])
- return [self legacyExpressionForStringDescribingRollModifier:dieRollString
- withDelimiterAtRange:lastDelimiterRange];
- else
- // This should be impossible.
- NSLog(@"IMPOSSIBLE CONDITION ENCOUNTERED WHILE PARSING DIE ROLL STRING!");
- }
-
- // If not an operation nor a roll command, the top-level term can only be
- // a simple numeric value.
- return [self legacyExpressionForStringDescribingNumericValue:dieRollString];
- }
-
- -(SA_DiceExpression *) legacyExpressionForStringDescribingOperation:(NSString *)dieRollString
- withOperatorString:(NSString *)operatorString
- atRange:(NSRange)operatorRange {
- SA_DiceExpression *expression = [SA_DiceExpression new];
-
- expression.type = SA_DiceExpressionTerm_OPERATION;
- expression.inputString = dieRollString;
-
- // Operands of a binary operator are the expressions generated by
- // parsing the strings before and after the addition operator.
- expression.leftOperand = [self legacyExpressionForLegalString:[dieRollString substringToIndex:operatorRange.location]];
- expression.rightOperand = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(operatorRange.location + operatorRange.length)]];
-
- if ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_PLUS] containsCharactersInString:operatorString]) {
- // Check to see if the term is an addition operation.
- expression.operator = SA_DiceExpressionOperator_PLUS;
- } else if([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] containsCharactersInString:operatorString]) {
- // Check to see if the term is a subtraction operation.
- expression.operator = SA_DiceExpressionOperator_MINUS;
- } else if([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_TIMES] containsCharactersInString:operatorString]) {
- // Check to see if the term is a multiplication operation.
- // 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 = [@[ [SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_PLUS],
- [SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] ]
- componentsJoinedByString:@""];
- NSRange lastLowerPrecedenceOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet
- characterSetWithCharactersInString:allLowerPrecedenceOperators]
- options:NSBackwardsSearch
- range:NSRangeMake(1, operatorRange.location - 1)];
- if (lastLowerPrecedenceOperatorRange.location != NSNotFound) {
- return [self legacyExpressionForStringDescribingOperation:dieRollString
- withOperatorString:[dieRollString substringWithRange:lastLowerPrecedenceOperatorRange]
- atRange:lastLowerPrecedenceOperatorRange];
- }
-
- expression.operator = SA_DiceExpressionOperator_TIMES;
- } else {
- expression.errorBitMask |= SA_DiceExpressionError_UNKNOWN_OPERATOR;
- }
-
- // 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).
- expression.errorBitMask |= expression.leftOperand.errorBitMask;
- expression.errorBitMask |= expression.rightOperand.errorBitMask;
-
- return expression;
- }
-
- -(SA_DiceExpression *) legacyExpressionForStringDescribingRollCommand:(NSString *)dieRollString
- withDelimiterAtRange:(NSRange)delimiterRange {
- SA_DiceExpression *expression = [SA_DiceExpression new];
-
- expression.type = SA_DiceExpressionTerm_ROLL_COMMAND;
- expression.inputString = dieRollString;
-
- // For now, only two kinds of roll command is supported - roll-and-sum,
- // and roll-and-sum with exploding dice.
- // These roll one or more dice of a given sort, and determine the sum of
- // their rolled values. (In the “exploding dice” version, each die can
- // explode, of course.)
- // In the future, support for other, more complex roll commands might be
- // added, such as “roll several and return the highest”.
- if ([[SA_DiceParser validCharactersForRollCommandDelimiter:SA_DiceExpressionRollCommand_SUM]
- containsString:[dieRollString substringWithRange:delimiterRange]])
- expression.rollCommand = SA_DiceExpressionRollCommand_SUM;
- else if ([[SA_DiceParser validCharactersForRollCommandDelimiter:SA_DiceExpressionRollCommand_SUM_EXPLODING]
- containsString:[dieRollString substringWithRange:delimiterRange]])
- expression.rollCommand = SA_DiceExpressionRollCommand_SUM_EXPLODING;
-
- // 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’).
- // Otherwise, the die count is the expression generated by parsing the
- // string before the delimiter.
- expression.dieCount = ((delimiterRange.location == 0) ?
- [self legacyExpressionForStringDescribingNumericValue:@"1"] :
- [self legacyExpressionForLegalString:[dieRollString substringToIndex:delimiterRange.location]]);
-
- // The die size is the expression generated by parsing the string after the
- // delimiter.
- expression.dieSize = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(delimiterRange.location + delimiterRange.length)]];
- if ([expression.dieSize.inputString.lowercaseString isEqualToString:@"f"])
- expression.dieType = SA_DiceExpressionDice_FUDGE;
-
- // 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.
- expression.errorBitMask |= expression.dieCount.errorBitMask;
- expression.errorBitMask |= expression.dieSize.errorBitMask;
-
- return expression;
- }
-
- -(SA_DiceExpression *) legacyExpressionForStringDescribingRollModifier:(NSString *)dieRollString
- withDelimiterAtRange:(NSRange)delimiterRange {
- SA_DiceExpression *expression = [SA_DiceExpression new];
-
- expression.type = SA_DiceExpressionTerm_ROLL_MODIFIER;
- expression.inputString = dieRollString;
-
- // The possible roll modifiers are KEEP HIGHEST and KEEP LOWEST.
- // These take a roll command and a number, and keep that number of rolls
- // generated by the roll command (either the highest or lowest rolls,
- // respectively).
- if ([[SA_DiceParser validCharactersForRollModifierDelimiter:SA_DiceExpressionRollModifier_KEEP_HIGHEST]
- containsString:[dieRollString substringWithRange:delimiterRange]])
- expression.rollModifier = SA_DiceExpressionRollModifier_KEEP_HIGHEST;
- else if ([[SA_DiceParser validCharactersForRollModifierDelimiter:SA_DiceExpressionRollModifier_KEEP_LOWEST]
- containsString:[dieRollString substringWithRange:delimiterRange]])
- expression.rollModifier = SA_DiceExpressionRollModifier_KEEP_LOWEST;
-
- // Check to see if the delimiter is the initial character of the roll
- // string. If so, set an error, because a roll modifier requires a
- // roll command to modify.
- if (delimiterRange.location == 0) {
- expression.errorBitMask |= SA_DiceExpressionError_ROLL_STRING_EMPTY;
- return expression;
- }
-
- // Otherwise, the left operand is the expression generated by parsing the
- // string before the delimiter.
- expression.leftOperand = [self legacyExpressionForLegalString:[dieRollString substringToIndex:delimiterRange.location]];
-
- // The right operand is the expression generated by parsing the string after
- // the delimiter.
- expression.rightOperand = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(delimiterRange.location + delimiterRange.length)]];
-
- // The left and right operands have now been parsed recursively; this
- // parsing may have generated one or more errors. Inherit any error(s) from
- // the error-generating sub-terms.
- expression.errorBitMask |= expression.leftOperand.errorBitMask;
- expression.errorBitMask |= expression.rightOperand.errorBitMask;
-
- return expression;
- }
-
- -(SA_DiceExpression *) legacyExpressionForStringDescribingNumericValue:(NSString *)dieRollString {
- SA_DiceExpression *expression = [SA_DiceExpression new];
-
- expression.type = SA_DiceExpressionTerm_VALUE;
- expression.inputString = dieRollString;
- if ([expression.inputString.lowercaseString isEqualToString:@"f"])
- expression.value = @(-1);
- else
- expression.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 {
- return [ @[ [SA_DiceParser validNumeralCharacters],
- [SA_DiceParser allValidRollCommandDelimiterCharacters],
- [SA_DiceParser allValidRollModifierDelimiterCharacters],
- [SA_DiceParser allValidOperatorCharacters] ] componentsJoinedByString:@""];
- }
-
- +(NSString *) allValidOperatorCharacters {
- NSDictionary *validOperatorCharactersDict = [SA_DiceParser validCharactersDict][SA_DB_VALID_OPERATOR_CHARACTERS];
-
- return [validOperatorCharactersDict.allValues componentsJoinedByString:@""];
- }
-
- +(NSString *) validCharactersForOperator:(SA_DiceExpressionOperator)operator {
- return [SA_DiceParser validCharactersDict][SA_DB_VALID_OPERATOR_CHARACTERS][NSStringFromSA_DiceExpressionOperator(operator)];
- }
-
- +(NSString *) validNumeralCharacters {
- return [SA_DiceParser validCharactersDict][SA_DB_VALID_NUMERAL_CHARACTERS];
- }
-
- +(NSString *) validCharactersForRollCommandDelimiter:(SA_DiceExpressionRollCommand)command {
- return [SA_DiceParser validCharactersDict][SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS][NSStringFromSA_DiceExpressionRollCommand(command)];
- }
-
- +(NSString *) allValidRollCommandDelimiterCharacters {
- NSDictionary *validRollCommandDelimiterCharactersDict = [SA_DiceParser validCharactersDict][SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS];
-
- return [validRollCommandDelimiterCharactersDict.allValues componentsJoinedByString:@""];
- }
-
- +(NSString *) validCharactersForRollModifierDelimiter:(SA_DiceExpressionRollModifier)modifier {
- return [SA_DiceParser validCharactersDict][SA_DB_VALID_ROLL_MODIFIER_DELIMITER_CHARACTERS][NSStringFromSA_DiceExpressionRollModifier(modifier)];
- }
-
- +(NSString *) allValidRollModifierDelimiterCharacters {
- NSDictionary *validRollModifierDelimiterCharactersDict = [SA_DiceParser validCharactersDict][SA_DB_VALID_ROLL_MODIFIER_DELIMITER_CHARACTERS];
-
- return [validRollModifierDelimiterCharactersDict.allValues componentsJoinedByString:@""];
- }
-
- @end
|