| #pragma mark - Properties | #pragma mark - Properties | ||||
| /************************/ | /************************/ | ||||
| // The expression’s type (operation, roll command, simple value, etc.). | |||||
| @property SA_DiceExpressionTermType type; | @property SA_DiceExpressionTermType type; | ||||
| @property NSUInteger errorBitMask; | |||||
| /*============================================================================== | |||||
| The following four sets of properties pertain to expressions of specific types. | |||||
| */ | |||||
| // Expressions of type SA_DiceExpressionTerm_OPERATION. | |||||
| @property SA_DiceExpressionOperator operator; | @property SA_DiceExpressionOperator operator; | ||||
| @property (nonatomic, strong) SA_DiceExpression *leftOperand; | @property (nonatomic, strong) SA_DiceExpression *leftOperand; | ||||
| @property (nonatomic, strong) SA_DiceExpression *rightOperand; | @property (nonatomic, strong) SA_DiceExpression *rightOperand; | ||||
| // Expressions of type SA_DiceExpressionTerm_ROLL_COMMAND. | |||||
| @property SA_DiceExpressionRollCommand rollCommand; | @property SA_DiceExpressionRollCommand rollCommand; | ||||
| @property (nonatomic, strong) SA_DiceExpression *dieCount; | @property (nonatomic, strong) SA_DiceExpression *dieCount; | ||||
| @property (nonatomic, strong) SA_DiceExpression *dieSize; | @property (nonatomic, strong) SA_DiceExpression *dieSize; | ||||
| @property SA_DiceExpressionDieType dieType; | @property SA_DiceExpressionDieType dieType; | ||||
| // Expressions of type SA_DiceExpressionTerm_ROLL_MODIFIER. | |||||
| @property SA_DiceExpressionRollModifier rollModifier; | @property SA_DiceExpressionRollModifier rollModifier; | ||||
| // Expressions of type SA_DiceExpressionTerm_VALUE. | |||||
| @property (nonatomic, strong) NSNumber *value; | @property (nonatomic, strong) NSNumber *value; | ||||
| /*=================================================== | |||||
| The following properties pertain to all expressions. | |||||
| */ | |||||
| @property SA_DiceExpressionError errorBitMask; | |||||
| @property (copy, nonatomic) NSString *inputString; | @property (copy, nonatomic) NSString *inputString; | ||||
| @property (copy, nonatomic) NSAttributedString *attributedInputString; | @property (copy, nonatomic) NSAttributedString *attributedInputString; | ||||
| /*========================================================================= | |||||
| The following properties pertain to evaluated expressions only. | |||||
| (They have a nil value for expressions which have not yet been evaluated.) | |||||
| */ | |||||
| // Evaluated expressions (of any type). | |||||
| @property (nonatomic, strong) NSNumber *result; | @property (nonatomic, strong) NSNumber *result; | ||||
| // Evaluated expressions of type SA_DiceExpressionTerm_ROLL_COMMAND. | |||||
| @property (nonatomic, strong) NSArray <NSNumber *> *rolls; | @property (nonatomic, strong) NSArray <NSNumber *> *rolls; | ||||
| /****************************/ | |||||
| #pragma mark - Public methods | |||||
| /****************************/ | |||||
| +(instancetype) expressionByJoiningExpression:(SA_DiceExpression *)leftHandExpression | |||||
| toExpression:(SA_DiceExpression *)rightHandExpression | |||||
| withOperator:(SA_DiceExpressionOperator)operator; | |||||
| @end | @end |
| #import "SA_DiceExpression.h" | #import "SA_DiceExpression.h" | ||||
| #import "SA_DiceFormatter.h" | |||||
| /*********************/ | /*********************/ | ||||
| #pragma mark Functions | #pragma mark Functions | ||||
| /*********************/ | /*********************/ | ||||
| @(SA_DiceExpressionError_INTEGER_UNDERFLOW_MULTIPLICATION) : @"SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION", | @(SA_DiceExpressionError_INTEGER_UNDERFLOW_MULTIPLICATION) : @"SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION", | ||||
| @(SA_DiceExpressionError_KEEP_COUNT_EXCEEDS_ROLL_COUNT) : @"SA_DB_ERROR_KEEP_COUNT_EXCEEDS_ROLL_COUNT", | @(SA_DiceExpressionError_KEEP_COUNT_EXCEEDS_ROLL_COUNT) : @"SA_DB_ERROR_KEEP_COUNT_EXCEEDS_ROLL_COUNT", | ||||
| @(SA_DiceExpressionError_KEEP_COUNT_NEGATIVE) : @"SA_DB_ERROR_KEEP_COUNT_NEGATIVE" | @(SA_DiceExpressionError_KEEP_COUNT_NEGATIVE) : @"SA_DB_ERROR_KEEP_COUNT_NEGATIVE" | ||||
| }; | }; | ||||
| }); | }); | ||||
| return SA_DiceExpressionErrorStringValues[@(error)]; | |||||
| __block NSMutableArray *errorStrings = [NSMutableArray array]; | |||||
| [SA_DiceExpressionErrorStringValues enumerateKeysAndObjectsUsingBlock:^(NSNumber *errorKey, | |||||
| NSString *errorString, | |||||
| BOOL *stop) { | |||||
| if (errorKey.unsignedIntegerValue & error) | |||||
| [errorStrings addObject:errorString]; | |||||
| }]; | |||||
| return [errorStrings componentsJoinedByString:@","]; | |||||
| // return SA_DiceExpressionErrorStringValues[@(error)]; | |||||
| } | } | ||||
| NSComparisonResult compareEvaluatedExpressionsByResult(SA_DiceExpression *expression1, | NSComparisonResult compareEvaluatedExpressionsByResult(SA_DiceExpression *expression1, | ||||
| @implementation SA_DiceExpression | @implementation SA_DiceExpression | ||||
| +(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. | |||||
| // TODO: Shouldn't the canonical representation of an operator | |||||
| // possibly vary by formatter behavior...? | |||||
| // But on the other hand, how does a parser know what formatter behavior | |||||
| // is in use...? | |||||
| // TODO: Parentheses aren't even supported by the legacy parser! | |||||
| // This is total nonsense! | |||||
| 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 - NSCopying methods | #pragma mark - NSCopying methods | ||||
| /*******************************/ | /*******************************/ |
| NOTE 2: Legacy mode does not support whitespace within roll strings. | NOTE 2: Legacy mode does not support whitespace within roll strings. | ||||
| TODO: Document exploding dice, Fudge dice, and ‘k’ and ‘l’ operators. | |||||
| ===================== | ===================== | ||||
| ==== MODERN mode ==== | ==== MODERN mode ==== | ||||
| ===================== | ===================== | ||||
| -(SA_DiceExpression *) expressionForString:(NSString *)dieRollString; | -(SA_DiceExpression *) expressionForString:(NSString *)dieRollString; | ||||
| -(SA_DiceExpression *) expressionByJoiningExpression:(SA_DiceExpression *)leftHandExpression | |||||
| toExpression:(SA_DiceExpression *)rightHandExpression | |||||
| withOperator:(SA_DiceExpressionOperator)operator; | |||||
| @end | @end |
| return _defaultParserBehavior; | return _defaultParserBehavior; | ||||
| } | } | ||||
| // TODO: Should this be on a per-mode, and therefore per-instance, basis? | |||||
| +(NSDictionary *) validCharactersDict { | +(NSDictionary *) validCharactersDict { | ||||
| if (_validCharactersDict == nil) { | if (_validCharactersDict == nil) { | ||||
| [SA_DiceParser loadValidCharactersDict]; | [SA_DiceParser loadValidCharactersDict]; | ||||
| } | } | ||||
| -(instancetype) initWithBehavior:(SA_DiceParserBehavior)parserBehavior { | -(instancetype) initWithBehavior:(SA_DiceParserBehavior)parserBehavior { | ||||
| if (self = [super init]) { | |||||
| self.parserBehavior = parserBehavior; | |||||
| if (_validCharactersDict == nil) { | |||||
| [SA_DiceParser loadValidCharactersDict]; | |||||
| } | |||||
| if (!(self = [super init])) | |||||
| return nil; | |||||
| self.parserBehavior = parserBehavior; | |||||
| if (_validCharactersDict == nil) { | |||||
| [SA_DiceParser loadValidCharactersDict]; | |||||
| } | } | ||||
| return self; | return self; | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| -(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 | #pragma mark - “Legacy” behavior implementation | ||||
| /**********************************************/ | /**********************************************/ | ||||
| // expression type fall through to the remaining cases (roll command | // expression type fall through to the remaining cases (roll command | ||||
| // or simple value). | // or simple value). | ||||
| // In the latter case, we register an error and return. | // In the latter case, we register an error and return. | ||||
| if (dieRollString.length == lastOperatorRange.length || | |||||
| ![[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] containsCharactersInString:operator]) { | |||||
| if ( dieRollString.length == lastOperatorRange.length | |||||
| || ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] containsCharactersInString:operator] == NO)) { | |||||
| SA_DiceExpression *expression = [SA_DiceExpression new]; | SA_DiceExpression *expression = [SA_DiceExpression new]; | ||||
| expression.type = SA_DiceExpressionTerm_OPERATION; | expression.type = SA_DiceExpressionTerm_OPERATION; | ||||
| expression.inputString = dieRollString; | expression.inputString = dieRollString; | ||||
| expression.errorBitMask |= SA_DiceExpressionError_INVALID_EXPRESSION; | expression.errorBitMask |= SA_DiceExpressionError_INVALID_EXPRESSION; | ||||
| if ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_PLUS] containsCharactersInString:operatorString]) { | if ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_PLUS] containsCharactersInString:operatorString]) { | ||||
| // Check to see if the term is an addition operation. | // Check to see if the term is an addition operation. | ||||
| expression.operator = SA_DiceExpressionOperator_PLUS; | expression.operator = SA_DiceExpressionOperator_PLUS; | ||||
| } else if([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] containsCharactersInString:operatorString]) { | |||||
| } else if ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_MINUS] containsCharactersInString:operatorString]) { | |||||
| // Check to see if the term is a subtraction operation. | // Check to see if the term is a subtraction operation. | ||||
| expression.operator = SA_DiceExpressionOperator_MINUS; | expression.operator = SA_DiceExpressionOperator_MINUS; | ||||
| } else if([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_TIMES] containsCharactersInString:operatorString]) { | |||||
| } else if ([[SA_DiceParser validCharactersForOperator:SA_DiceExpressionOperator_TIMES] containsCharactersInString:operatorString]) { | |||||
| // Check to see if the term is a multiplication operation. | // Check to see if the term is a multiplication operation. | ||||
| // Look for other, lower-precedence operators to the left of the | // Look for other, lower-precedence operators to the left of the | ||||
| // multiplication operator. If found, split the string there | // multiplication operator. If found, split the string there | ||||
| // These roll one or more dice of a given sort, and determine the sum of | // 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 | // their rolled values. (In the “exploding dice” version, each die can | ||||
| // explode, of course.) | // 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] | if ([[SA_DiceParser validCharactersForRollCommandDelimiter:SA_DiceExpressionRollCommand_SUM] | ||||
| containsString:[dieRollString substringWithRange:delimiterRange]]) | containsString:[dieRollString substringWithRange:delimiterRange]]) | ||||
| expression.rollCommand = SA_DiceExpressionRollCommand_SUM; | expression.rollCommand = SA_DiceExpressionRollCommand_SUM; | ||||
| } | } | ||||
| } | } | ||||
| // TODO: Should this be on a per-mode, and therefore per-instance, basis? | |||||
| +(NSString *) allValidCharacters { | +(NSString *) allValidCharacters { | ||||
| return [ @[ [SA_DiceParser validNumeralCharacters], | return [ @[ [SA_DiceParser validNumeralCharacters], | ||||
| [SA_DiceParser allValidRollCommandDelimiterCharacters], | [SA_DiceParser allValidRollCommandDelimiterCharacters], |