commit 5fcd8bdbd3111517df8525874b2b5cf9500c59fa Author: achmizs Date: Sat Jan 16 14:58:30 2016 -0500 Initial commit. diff --git a/SA_DB_ErrorDescriptions.plist b/SA_DB_ErrorDescriptions.plist new file mode 100644 index 0000000..4c1338d --- /dev/null +++ b/SA_DB_ErrorDescriptions.plist @@ -0,0 +1,38 @@ + + + + + SA_DB_ERROR_ROLL_STRING_EMPTY + Empty expression + SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS + Illegal characters detected + SA_DB_ERROR_UNKNOWN_ROLL_COMMAND + Unknown roll command + SA_DB_ERROR_DIE_COUNT_NEGATIVE + Die count negative + SA_DB_ERROR_DIE_COUNT_EXCESSIVE + Too many dice + SA_DB_ERROR_DIE_SIZE_INVALID + Invalid die size (zero or negative) + SA_DB_ERROR_DIE_SIZE_EXCESSIVE + Die size too big + SA_DB_ERROR_UNKNOWN_OPERATOR + Unknown operator + SA_DB_ERROR_INVALID_EXPRESSION + Invalid expression + SA_DB_ERROR_INTEGER_OVERFLOW_NEGATION + Integer overflow during negation + SA_DB_ERROR_INTEGER_OVERFLOW_ADDITION + Integer overflow during addition + SA_DB_ERROR_INTEGER_UNDERFLOW_ADDITION + Integer underflow during addition + SA_DB_ERROR_INTEGER_OVERFLOW_SUBTRACTION + Integer overflow during subtraction + SA_DB_ERROR_INTEGER_UNDERFLOW_SUBTRACTION + Integer underflow during subtraction + SA_DB_ERROR_INTEGER_OVERFLOW_MULTIPLICATION + Integer overflow during multiplication + SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION + Integer underflow during multiplication + + diff --git a/SA_DB_StringFormatRules.plist b/SA_DB_StringFormatRules.plist new file mode 100644 index 0000000..2597f6f --- /dev/null +++ b/SA_DB_StringFormatRules.plist @@ -0,0 +1,36 @@ + + + + + SA_DB_VALID_CHARACTERS + + SA_DB_VALID_NUMERAL_CHARACTERS + 0123456789 + SA_DB_VALID_OPERATOR_CHARACTERS + + SA_DB_OPERATOR_MINUS + -–− + SA_DB_OPERATOR_PLUS + + + SA_DB_OPERATOR_TIMES + *×⋅· + + SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS + dD + + SA_DB_CANONICAL_REPRESENTATIONS + + SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS + + SA_DB_OPERATOR_MINUS + - + SA_DB_OPERATOR_PLUS + + + SA_DB_OPERATOR_TIMES + * + + SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION + d + + + diff --git a/SA_DiceBag.h b/SA_DiceBag.h new file mode 100644 index 0000000..51fb9e5 --- /dev/null +++ b/SA_DiceBag.h @@ -0,0 +1,26 @@ +// +// SA_DiceBag.h +// RPGBot +// +// Created by Sandy Achmiz on 12/30/15. +// +// + +#import + +/****************************************/ +#pragma mark SA_DiceBag class declaration +/****************************************/ + +@interface SA_DiceBag : NSObject + +/****************************/ +#pragma mark - Public methods +/****************************/ + +- (unsigned long long)biggestPossibleDieSize; + +- (unsigned long long)rollDie:(unsigned long long)die; +- (NSArray *)rollNumber:(NSNumber *)number ofDice:(unsigned long long)die; + +@end diff --git a/SA_DiceBag.m b/SA_DiceBag.m new file mode 100644 index 0000000..7c41b9a --- /dev/null +++ b/SA_DiceBag.m @@ -0,0 +1,54 @@ +// +// SA_DiceBag.m +// RPGBot +// +// Created by Sandy Achmiz on 12/30/15. +// +// + +#import "SA_DiceBag.h" + +/*******************************************/ +#pragma mark SA_DiceBag class implementation +/*******************************************/ + +@implementation SA_DiceBag + +/****************************/ +#pragma mark - Public methods +/****************************/ + +- (unsigned long long)biggestPossibleDieSize +{ + return UINT32_MAX; +} + +- (unsigned long long)rollDie:(unsigned long long)dieSize +{ + if(dieSize > UINT32_MAX) + { + return -1; + } + + return (unsigned long long) arc4random_uniform((u_int32_t) dieSize) + 1; +} + +- (NSArray *)rollNumber:(NSNumber *)number ofDice:(unsigned long long)dieSize +{ + if(dieSize > UINT32_MAX) + { + return nil; + } + + unsigned long long numRolls = number.unsignedLongLongValue; + + NSMutableArray *rollsArray = [NSMutableArray arrayWithCapacity:numRolls]; + for(unsigned long long i = 0; i < numRolls; i++) + { + rollsArray[i] = @((unsigned long long) arc4random_uniform((u_int32_t) dieSize) + 1); + } + + return rollsArray; +} + +@end diff --git a/SA_DiceBot.h b/SA_DiceBot.h new file mode 100644 index 0000000..7d1384b --- /dev/null +++ b/SA_DiceBot.h @@ -0,0 +1,183 @@ +// +// SA_DiceBot.h +// RPGBot +// +// Created by Sandy Achmiz on 12/30/15. +// +// +/* + The SA_DiceBot class hierarchy works like this: + + SA_DiceBot is the root class. A user of the SA_DiceBot package usually only + needs to create and manage objects of this class. An SA_DiceBot creates and + manages all other objects and classes in the package (directly or through its + other members), as needed. + + An SA_DiceBot owns a single SA_DiceRoller and a single SA_DiceFormatter. + + The objects owned by an SA_DiceBot are for the SA_DiceBot's internal use + only, and not accessible to users of the package. (But see SA_DiceRoller.h + and SA_DiceFormatter.h for information on standalone use.) + + An SA_DiceRoller owns an SA_DiceParser and an SA_DiceEvaluator. An + SA_DiceEvaluator owns an SA_DiceBag. (See Figure 1, below.) + + *--------------------------------------------------------------------------* + | Fig. 1. SA_DiceBot class hierarchy diagram. | + | | + | | + | SA_DiceBot | + | / \ | + | SA_DiceFormatter SA_DiceRoller | + | / \ | + | SA_DiceParser SA_DiceEvaluator | + | | | + | SA_DiceBag | + | | + *--------------------------------------------------------------------------* + + When the SA_DiceBot receives a recognized command that involves rolling some + dice or otherwise evaluating some sort of string of semantically meaningful + text (like just adding some numbers), it extracts that string (which may + optionally include some text label) from the rest of the message body, and + passes it to the SA_DiceRoller. + + The SA_DiceRoller strips off the label (if any), and passes the 'pure' string + to the SA_DiceParser. + + The SA_DiceParser parses the string and produces an 'expression tree' + representation of the string. An expression tree is an NSDictionary with a + certain structure (which is described in SA_DiceExpressionStringConstants.h). + + The SA_DiceRoller then passes the expression tree to the SA_DiceEvaluator. + The evaluator recursively traverses the expression tree in a depth-first + manner, computing the results of each subtree, and storing those results in + (a mutable copy of) the expression tree itself. Once the entire tree has been + evaluated, (a mutable copy of) it is returned to the SA_DiceRoller. The + SA_DiceRoller reattaches the stripped-off label (inserting it into the top + level of the expresion tree) and returns the evaluated expression tree + (now called a result) to the SA_DiceBot. + + The SA_DiceBot passes the evaluated result tree to its SA_DiceFormatter. + The formatter traverses the tree, constructing a human-readable string form of + the results, with whatever formatting it (the formatter) has been configured + to provide. The formatter then returns this formatted result string to the + SA_DiceBot. + + The SA_DiceBot then incorporates the formatted result string into some + appropriate reply message or messages, and sends the reply(ies) back to its + delegate, for transmission to the appropriate endpoint. + */ + +#import +#import "SA_Bot.h" + +@class SA_CommandResponder; + +/* + ###################################### + #### SA_DiceBot Usage Information #### + ###################################### + + I. SUPPORTED COMMANDS & PARSER MODES + + The set of commands, and the syntax and behavior of those commands, that an + SA_DiceBot supports depends on its currently set parser behavior mode (and, in + some cases, also on the currently set formatter behavior mode). (Read more + about parser behavior modes in SA_DiceParser.h, and about formatter behavior + modes in SA_DiceFormatter.h.) + + Below is a list of available parser modes, along with the commands supported in + each mode. (See section II for a list of commands that are supported in all + parser modes.) + + NOTE: Commands are not case-sensitive; e.g., 'roll', 'ROLL', and 'rOLl' all + work equally well. + + 1. DEFAULT mode + + "Default" mode is an alias for whatever default behavior is currently set for + new SA_DiceParser instances. (The "default default" behavior for the current + implementation is "legacy".) + + 2. LEGACY mode + + "Legacy" mode (mostly) emulates DiceBot by Sabin (and Dawn by xthemage before + it). The following commands are available in legacy mode. + + 1. ROLL command. + + Takes 1 or more whitespace-delimited roll strings as parameters. The roll + command is executed once for every parameter, and each execution generates + a separate result string. One reply message is sent for each result string + (or, if the simple formatter is being used, the results may optionally be + collapsed into a single reply message). + + Each roll string may optionally be suffixed with with a (configurable) + delimiter character (such as ';'), which may be followed with an arbitrary + text label (which may not contain whitespace, however). That label may then + be included in the result string (depending on the selected formatter + behavior mode and other formatter settings). + + The body of the roll string (up to the label delimiter, if any) is simply + parsed and evaluated. + + EXAMPLES (assuming legacy formatter behavior): + + Obormot: !roll 1d20 + SA_DiceBot: 1d20 < 14 = 14 > = 14 + Obormot: .roll 2+4-5 + SA_DiceBot: 2 + 4 - 5 = 1 + Obormot: /roll 2d4 1d20+19 4d10 + SA_DiceBot: 2d4 < 3 1 = 4 > = 4 + SA_DiceBot: 1d20 < 5 = 5 > + 19 = 24 + SA_DiceBot: 4d10 < 2 2 1 2 = 7 > = 7 + Obormot: !roll 1d20+4;fort_save + SA_DiceBot: (fort_save) 1d20 < 8 = 8 > + 4 = 12 + Obormot: SA_DiceBot: 1d8+6;longsword 1d6+3;shortsword + SA_DiceBot: (longsword) 1d8 < 4 = 4 > + 6 = 10 + SA_DiceBot: (shortsword) 1d6 < 2 = 2 > + 3 = 5 + + 2. TRY command. + + Takes 1 or more whitespace-delimited roll strings. Prepends "1d20+" to each + roll string, and otherwise behaves in the same way as the ROLL command. + + 3. CHAR command. + + II. OTHER COMMANDS + + III. CONFIGURATION + */ +/****************************************/ +#pragma mark SA_DiceBot class declaration +/****************************************/ + +@interface SA_DiceBot : SA_Bot + +/************************/ +#pragma mark - Properties +/************************/ + +@property (strong) SA_CommandResponder *botCommandresponder; +@property (strong) SA_CommandResponder *legacyCommandResponder; + +@property (strong) SA_CommandResponder *currentCommandResponder; + +@property (copy) NSString *commandDesignatorCharacters; + +/****************************/ +#pragma mark - Public methods +/****************************/ + +- (void)message:(NSString *)messageBody withInfo:(NSDictionary *)messageInfo; + +/****************************/ +#pragma mark - Helper methods +/****************************/ + +- (NSArray *)repliesForCommandString:(NSString *)commandString messageInfo:(NSDictionary *)messageInfo byName:(BOOL)byName; + +- (void)loadDefaultCommandResponders; + +@end diff --git a/SA_DiceBot.m b/SA_DiceBot.m new file mode 100644 index 0000000..dfc5717 --- /dev/null +++ b/SA_DiceBot.m @@ -0,0 +1,144 @@ +// +// SA_DiceBot.m +// RPGBot +// +// Created by Sandy Achmiz on 12/30/15. +// +// + +#import "SA_DiceBot.h" + +#import "SA_BotDelegate.h" +#import "SA_CommandResponder.h" +#import "SA_BotCommandResponder.h" +#import "SA_LegacyCommandResponder.h" +#import "SA_ErrorCatalog.h" +#import "NSString+SA_NSStringExtensions.h" +#import "NSRange-Conventional.h" + +/*********************************************/ +#pragma mark - SA_DiceBot class implementation +/*********************************************/ + +@implementation SA_DiceBot + +/**************************/ +#pragma mark - Initializers +/**************************/ + +- (instancetype)init +{ + return [self initWithName:@"DIE_BOT"]; +} + +- (instancetype)initWithName:(NSString *)name +{ + if(self = [super initWithName:name]) + { + NSLog(NSLocalizedString(@"Initializing SA_DiceBot with name \"%@\"", @"{display name of the bot}"), name); + + self.commandDesignatorCharacters = @".!/"; + + [self loadDefaultCommandResponders]; + } + return self; +} + +/****************************/ +#pragma mark - Public methods +/****************************/ + +- (void)message:(NSString *)messageBody withInfo:(NSDictionary *)messageInfo +{ + if([messageBody isEqualToString:@""]) + { + // Ignore empty messages. + } + else + { + NSRange commandRange; + BOOL byName; + + // Is the message a possible command? That is, does it start with any of + // the permitted initial characters that designate a command? + NSString *firstChar = [messageBody substringToIndex:1]; + if([self.commandDesignatorCharacters containsCharactersInString:firstChar]) + { + commandRange = NSMakeRange(1, messageBody.length - 1); + byName = NO; + } + else + { + // We also recognize commands that come after mentions of the bot's + // name at the beginning of the message. + NSRange possibleNameRange = NSRangeMake(0, self.name.length); + if(messageBody.length > self.name.length && + [[messageBody substringWithRange:possibleNameRange] isEqualToString:self.name]) + { + commandRange = [messageBody rangeToEndFrom:[messageBody firstNonWhitespaceAfterRange:[messageBody firstWhitespaceAfterRange:possibleNameRange]]]; + byName = YES; + } + else + { + // Does not begin with a command. Ignore. + return; + } + } + + // Extract the part of the string that is the actual command. + NSString *commandString = [messageBody substringWithRange:commandRange]; + + // Get the replies for this command. + NSArray *replies = [self repliesForCommandString:commandString messageInfo:messageInfo byName:byName]; + + // Send the replies. + [replies enumerateObjectsUsingBlock:^(NSDictionary *reply, NSUInteger idx, BOOL *stop) { + [self.delegate SA_botMessage:reply[SA_DB_MESSAGE_BODY] + from:self + withInfo:reply[SA_DB_MESSAGE_INFO]]; + }]; + } +} + +/****************************/ +#pragma mark - Helper methods +/****************************/ + +- (NSArray *)repliesForCommandString:(NSString *)commandString messageInfo:(NSDictionary *)messageInfo byName:(BOOL)byName +{ + NSError *error; + NSArray *replies = [self.botCommandresponder repliesForCommandString:commandString messageInfo:messageInfo error:&error]; + + if(error && error.code == SA_DiceBotErrorUnknownCommand) + { + error = nil; + replies = [self.currentCommandResponder repliesForCommandString:commandString messageInfo:messageInfo error:&error]; + } + + if(error) + { + // Is outputting the provided error the right way to do error handling + // here? I don't know. Maybe not. For now, that's what it is. + NSString *errorReply = [NSString stringWithFormat:NSLocalizedString(@"ERROR: %@ (%@ %@)", @"{description}, {failure reason}, {recovery suggestion}"), + error.localizedDescription, + error.localizedFailureReason, + error.localizedRecoverySuggestion]; + + replies = [replies arrayByAddingObject:@{ SA_DB_MESSAGE_BODY : errorReply, + SA_DB_MESSAGE_INFO : messageInfo }]; + } + + return replies; +} + +- (void)loadDefaultCommandResponders +{ + self.legacyCommandResponder = [SA_LegacyCommandResponder new]; + self.botCommandresponder = [SA_BotCommandResponder new]; + + // The default command responder, in the current implementation, is the + // legacy command responder. + self.currentCommandResponder = self.legacyCommandResponder; +} + +@end diff --git a/SA_DiceErrorHandling.h b/SA_DiceErrorHandling.h new file mode 100644 index 0000000..f5e06a3 --- /dev/null +++ b/SA_DiceErrorHandling.h @@ -0,0 +1,18 @@ +// +// SA_DiceErrorHandling.h +// RPGBot +// +// Created by Sandy Achmiz on 1/11/16. +// +// + +#ifndef SA_DiceErrorHandling_h +#define SA_DiceErrorHandling_h + +#import + +void addErrorToExpression (NSString *error, NSMutableDictionary *expression); +void addErrorsFromExpressionToExpression (NSDictionary *sourceExpression, NSMutableDictionary *targetExpression); +NSArray *getErrorsForExpression (NSDictionary *expression); + +#endif /* SA_DiceErrorHandling_h */ diff --git a/SA_DiceErrorHandling.m b/SA_DiceErrorHandling.m new file mode 100644 index 0000000..1a387d4 --- /dev/null +++ b/SA_DiceErrorHandling.m @@ -0,0 +1,56 @@ +// +// SA_DiceErrorHandling.m +// RPGBot +// +// Created by Sandy Achmiz on 1/11/16. +// +// + +#import "SA_DiceErrorHandling.h" + +#import "SA_DiceExpressionStringConstants.h" + +void addErrorToExpression (NSString *error, NSMutableDictionary *expression) +{ + if(error == nil || expression == nil) + { + return; + } + + if(expression[SA_DB_ERRORS] == nil) + { + expression[SA_DB_ERRORS] = [NSMutableArray arrayWithObjects:error, nil]; + } + else + { + [expression[SA_DB_ERRORS] addObject:error]; + } +} + +// Top-level errors only (i.e. the expression tree is not traversed in search +// of deeper errors). +void addErrorsFromExpressionToExpression (NSDictionary *sourceExpression, NSMutableDictionary *targetExpression) +{ + if(sourceExpression == nil || targetExpression == nil) + { + return; + } + + if(sourceExpression[SA_DB_ERRORS] == nil || [sourceExpression[SA_DB_ERRORS] count] == 0) + { + // Do absolutely nothing; no errors to add. + } + else if(targetExpression[SA_DB_ERRORS] == nil) + { + targetExpression[SA_DB_ERRORS] = [NSMutableArray arrayWithArray:sourceExpression[SA_DB_ERRORS]]; + } + else + { + [targetExpression[SA_DB_ERRORS] addObjectsFromArray:sourceExpression[SA_DB_ERRORS]]; + } +} + +NSArray *getErrorsForExpression (NSDictionary *expression) +{ + return ([expression[SA_DB_ERRORS] count] > 0) ? expression[SA_DB_ERRORS] : nil; +} diff --git a/SA_DiceEvaluator.h b/SA_DiceEvaluator.h new file mode 100644 index 0000000..7b64e20 --- /dev/null +++ b/SA_DiceEvaluator.h @@ -0,0 +1,34 @@ +// +// SA_DiceEvaluator.h +// RPGBot +// +// Created by Sandy Achmiz on 1/2/16. +// +// + +#import + +@class SA_DiceBag; + +/************************************************/ +#pragma mark SA_DiceEvaluator class declaration +/************************************************/ + +@interface SA_DiceEvaluator : NSObject + +/************************/ +#pragma mark - Properties +/************************/ + +@property NSInteger maxDieCount; +@property NSInteger maxDieSize; + +@property (strong) SA_DiceBag *diceBag; + +/****************************/ +#pragma mark - Public methods +/****************************/ + +- (NSMutableDictionary *)resultOfExpression:(NSDictionary *)expression; + +@end diff --git a/SA_DiceEvaluator.m b/SA_DiceEvaluator.m new file mode 100644 index 0000000..85d9c37 --- /dev/null +++ b/SA_DiceEvaluator.m @@ -0,0 +1,371 @@ +// +// SA_DiceEvaluator.m +// RPGBot +// +// Created by Sandy Achmiz on 1/2/16. +// +// + +#import "SA_DiceEvaluator.h" + +#import "SA_DiceBag.h" +#import "SA_DiceParser.h" +#import "SA_DiceExpressionStringConstants.h" +#import "SA_DiceErrorHandling.h" + +/**************************/ +#pragma mark Defined values +/**************************/ + +#define DEFAULT_MAX_DIE_COUNT 1000 // One thousand +#define DEFAULT_MAX_DIE_SIZE 10000 // Ten thousand + +/***************************************************/ +#pragma mark - SA_DiceEvaluator class implementation +/***************************************************/ + +@implementation SA_DiceEvaluator +{ + NSInteger _maxDieCount; + NSInteger _maxDieSize; +} + +/************************/ +#pragma mark - Properties +/************************/ + +- (NSInteger)maxDieCount +{ + return _maxDieCount; +} +- (void)setMaxDieCount:(NSInteger)maxDieCount +{ + if(maxDieCount < 1) + { + _maxDieCount = 1; + } + else if(maxDieCount > NSIntegerMax / _maxDieSize) + { + _maxDieCount = NSIntegerMax / _maxDieSize; + } + else + { + _maxDieCount = maxDieCount; + } +} + +- (NSInteger)maxDieSize +{ + return _maxDieSize; +} +- (void)setMaxDieSize:(NSInteger)maxDieSize +{ + if(maxDieSize < 1) + { + _maxDieSize = 1; + } + else if(maxDieSize > [self.diceBag biggestPossibleDieSize] || maxDieSize > NSIntegerMax / _maxDieCount) + { + _maxDieSize = ([self.diceBag biggestPossibleDieSize] < (NSIntegerMax / _maxDieCount)) ? [_diceBag biggestPossibleDieSize] : NSIntegerMax / _maxDieCount; + } + else + { + _maxDieSize = maxDieSize; + } +} + +/**************************/ +#pragma mark - Initializers +/**************************/ + +- (instancetype)init +{ + if(self = [super init]) + { + _maxDieCount = DEFAULT_MAX_DIE_COUNT; + _maxDieSize = DEFAULT_MAX_DIE_SIZE; + + self.diceBag = [[SA_DiceBag alloc] init]; + } + return self; +} + +/****************************/ +#pragma mark - Public methods +/****************************/ + +- (NSMutableDictionary *)resultOfExpression:(NSDictionary *)expression +{ + // Check to see if the expression is erroneous (i.e. the parser has judged + // that it is malformed, etc.). If so, decline to evaluate the expression; + // return (a mutable copy of) it, unchanged. + if(getErrorsForExpression(expression) != nil) + { + return [expression mutableCopy]; + } + + /* + Even if an expression is not erroneous (i.e. if it has no syntax errors), + it may still not be possible to evaluate it. For example, '5d0' is a + perfectly well-formed die string, and will yield an expression tree as follows: + + @{SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_ROLL_COMMAND, + SA_DB_ROLL_COMMAND : SA_DB_ROLL_COMMAND_SUM, + SA_DB_ROLL_DIE_COUNT : @{SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_VALUE, + SA_DB_VALUE : @(5) + }, + SA_DB_ROLL_DIE_SIZE : @{SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_VALUE, + SA_DB_VALUE : @(0) + } + } + + This is, of course, an illegal expression; we can't roll a die of size 0 + (a die with zero sides?). + + If we encounter such an illegal expression, we add an appropriate error to + the term. We are not required to set a value for the SA_DB_RESULT key in + such a case. + */ + + // Check to see if the current term is an operation. + if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_OPERATION]) + { + return [self resultOfExpressionDescribingOperation:expression]; + } + // Check to see if the current term is a roll command. + else if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_ROLL_COMMAND]) + { + return [self resultOfExpressionDescribingRollCommand:expression]; + } + // If not an operation or a roll command, the current term can only be a + // simple value expression (term type of SA_DB_TERM_TYPE_VALUE). + else + { + return [self resultOfExpressionDescribingValue:expression]; + } +} + +/****************************/ +#pragma mark - Helper methods +/****************************/ + +- (NSMutableDictionary *)resultOfExpressionDescribingValue:(NSDictionary *)expression +{ + NSMutableDictionary *result = [expression mutableCopy]; + + result[SA_DB_RESULT] = result[SA_DB_VALUE]; + + return result; +} + +- (NSMutableDictionary *)resultOfExpressionDescribingRollCommand:(NSDictionary *)expression +{ + NSMutableDictionary *result = [expression mutableCopy]; + + // For now, only a simple sum is supported. Other sorts of roll commands + // may be added later. + if([result[SA_DB_ROLL_COMMAND] isEqualToString:SA_DB_ROLL_COMMAND_SUM]) + { + // First, recursively evaluate the expressions that represent the + // die size and die count. + result[SA_DB_ROLL_DIE_COUNT] = [self resultOfExpression:result[SA_DB_ROLL_DIE_COUNT]]; + result[SA_DB_ROLL_DIE_SIZE] = [self resultOfExpression:result[SA_DB_ROLL_DIE_SIZE]]; + + // Evaluating those expressions may have generated an error(s); + // propagate any errors up to the current term. + addErrorsFromExpressionToExpression(result[SA_DB_ROLL_DIE_COUNT], result); + addErrorsFromExpressionToExpression(result[SA_DB_ROLL_DIE_SIZE], result); + + // If indeed we've turned up errors, return. + if(getErrorsForExpression(result) != nil) + { + return result; + } + + // Evaluating the two sub-expressions didn't generate errors; this means + // that we have successfully generated values for both the die count and + // the die size... + NSInteger dieCount = [result[SA_DB_ROLL_DIE_COUNT][SA_DB_RESULT] integerValue]; + NSInteger dieSize = [result[SA_DB_ROLL_DIE_SIZE][SA_DB_RESULT] integerValue]; + + // ... but, the resulting values of the expressions may make it + // impossible to evaluate the roll command. Check to see whether the die + // count and die size have legal values. + if(dieCount < 0) + { + addErrorToExpression(SA_DB_ERROR_DIE_COUNT_NEGATIVE, result); + } + else if(dieCount > _maxDieCount) + { + addErrorToExpression(SA_DB_ERROR_DIE_COUNT_EXCESSIVE, result); + } + + if(dieSize < 1) + { + addErrorToExpression(SA_DB_ERROR_DIE_SIZE_INVALID, result); + } + else if(dieSize > _maxDieSize) + { + addErrorToExpression(SA_DB_ERROR_DIE_SIZE_EXCESSIVE, result); + } + + // If indeed the die count or die size fall outside of their allowed + // ranges, return. + if(getErrorsForExpression(result) != nil) + { + return result; + } + + // The die count and die size have legal values. We can safely roll the + // requisite number of dice, and take the sum of the rolls (if needed). + // NOTE: _maxDieSize is gauranteed to be no greater than the largest die + // size that the SA_DiceBag can roll (this is enforced by the setter + // method for the maxDieSize property), so we need not check to see + // if the return value of rollDie: or rollNumber:ofDice: is valid. + // We are also gauranteed that the product of _maxDieCount and + // _maxDieSize is no greater than the largest unsigned value that can be + // stored by whatever numeric type we specify simple value terms (terms + // of type SA_DB_TERM_TYPE_VALUE) to contain (likewise enforced by the + // setters for both maxDieSize and maxDieCount), therefore we need not + // worry about overflow here. + if(dieCount == 0) + { + result[SA_DB_RESULT] = @(0); + result[SA_DB_ROLLS] = @[]; + } + else if(dieCount == 1) + { + NSNumber *roll = @([self.diceBag rollDie:dieSize]); + + result[SA_DB_RESULT] = roll; + result[SA_DB_ROLLS] = @[roll]; + } + else + { + NSArray *rolls = [self.diceBag rollNumber:@(dieCount) ofDice:dieSize]; + + result[SA_DB_RESULT] = [rolls valueForKeyPath:@"@sum.self"]; + result[SA_DB_ROLLS] = rolls; + } + + // Return the successfully evaluated roll command expression. + return result; + } + else + { + addErrorToExpression(SA_DB_ERROR_UNKNOWN_ROLL_COMMAND, result); + + return result; + } +} + +- (NSMutableDictionary *)resultOfExpressionDescribingOperation:(NSDictionary *)expression +{ + NSMutableDictionary *result = [expression mutableCopy]; + + // First, recursively evaluate the expressions that represent the + // left-hand-side and right-hand-side operands. + result[SA_DB_OPERAND_LEFT] = [self resultOfExpression:result[SA_DB_OPERAND_LEFT]]; + result[SA_DB_OPERAND_RIGHT] = [self resultOfExpression:result[SA_DB_OPERAND_RIGHT]]; + + // Evaluating the operand may have generated an error(s); propagate any + // errors up to the current term. + addErrorsFromExpressionToExpression(result[SA_DB_OPERAND_LEFT], result); + addErrorsFromExpressionToExpression(result[SA_DB_OPERAND_RIGHT], result); + + // If indeed we've turned up errors, return. + if(getErrorsForExpression(result) != nil) + { + return result; + } + + // Evaluating the operands didn't generate any errors. We have valid + // operands. + NSInteger leftOperand = [result[SA_DB_OPERAND_LEFT][SA_DB_RESULT] integerValue]; + NSInteger rightOperand = [result[SA_DB_OPERAND_RIGHT][SA_DB_RESULT] integerValue]; + + // Check to see if the operation is subtraction. + if([result[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_MINUS]) + { + // First, we check for possible overflow... + if(leftOperand > 0 && rightOperand < 0 && NSIntegerMax + rightOperand < leftOperand) + { + addErrorToExpression(SA_DB_ERROR_INTEGER_OVERFLOW_SUBTRACTION, result); + + return result; + } + else if(leftOperand < 0 && rightOperand > 0 && NSIntegerMin + rightOperand > leftOperand) + { + addErrorToExpression(SA_DB_ERROR_INTEGER_UNDERFLOW_SUBTRACTION, result); + + return result; + } + + // No overflow will occur. We can perform the subtraction operation. + result[SA_DB_RESULT] = @(leftOperand - rightOperand); + + // Return the successfully evaluated negation expression. + return result; + } + // Check to see if the operation is addition. + else if([result[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_PLUS]) + { + // First, we check for possible overflow... + if(rightOperand > 0 && leftOperand > 0 && NSIntegerMax - rightOperand < leftOperand) + { + addErrorToExpression(SA_DB_ERROR_INTEGER_OVERFLOW_ADDITION, result); + + return result; + } + else if(rightOperand < 0 && leftOperand < 0 && NSIntegerMin - rightOperand > leftOperand) + { + addErrorToExpression(SA_DB_ERROR_INTEGER_UNDERFLOW_ADDITION, result); + + return result; + } + + // No overflow will occur. We can perform the addition operation. + result[SA_DB_RESULT] = @(leftOperand + rightOperand); + + // Return the successfully evaluated addition expression. + return result; + } + // Check to see if the operation is multiplication. + else if([result[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_TIMES]) + { + // First, we check for possible overflow... + if( ( leftOperand == NSIntegerMin && ( rightOperand != 0 || rightOperand != 1 ) ) || + ( rightOperand == NSIntegerMin && ( leftOperand != 0 || leftOperand != 1 ) ) || + ( leftOperand != 0 && ( (NSIntegerMax / ABS(leftOperand)) < rightOperand ) ) ) + { + if((leftOperand > 0 && rightOperand > 0) || (leftOperand < 0 && rightOperand < 0)) + { + addErrorToExpression(SA_DB_ERROR_INTEGER_OVERFLOW_MULTIPLICATION, result); + } + else + { + addErrorToExpression(SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION, result); + } + + return result; + } + + // No overflow will occur. We can perform the multiplication operation. + result[SA_DB_RESULT] = @(leftOperand * rightOperand); + + // Return the successfully evaluated multiplication expression. + return result; + } + // The operation is not one of the supported operators. + else + { + // We add the appropriate error. We do not set a value for the + // SA_DB_RESULT key. + + addErrorToExpression(SA_DB_ERROR_UNKNOWN_OPERATOR, result); + + return result; + } +} + +@end diff --git a/SA_DiceExpressionStringConstants.h b/SA_DiceExpressionStringConstants.h new file mode 100644 index 0000000..5cf3d14 --- /dev/null +++ b/SA_DiceExpressionStringConstants.h @@ -0,0 +1,167 @@ +// +// SA_DiceExpressionStringConstants.h +// RPGBot +// +// Created by Sandy Achmiz on 1/2/16. +// +// + +#import + +/***************************************************/ +#pragma mark String constants for expression parsing +/***************************************************/ + +// Each term in an expression (i.e. each node in the expression tree) has a +// value for the SA_DB_TERM_TYPE key, which may be one of the term types listed +// below. SA_DB_TERM_TYPE_NONE is used for terms that fail to be parsed at all, +// because their input strings contain illegal characters or are empty. +extern NSString * const SA_DB_TERM_TYPE; // key +extern NSString * const SA_DB_TERM_TYPE_VALUE; // value +extern NSString * const SA_DB_TERM_TYPE_ROLL_COMMAND; // value +extern NSString * const SA_DB_TERM_TYPE_OPERATION; // value +extern NSString * const SA_DB_TERM_TYPE_NONE; // value + +// Terms that are erroneous (malformed or illegal in some way) have a value for +// the SA_DB_ERRORS key. Whether they have values for any other keys is +// undefined. The value for this key is an NSArray of NSString objects, each +// having one of the listed values. +extern NSString * const SA_DB_ERRORS; // key +extern NSString * const SA_DB_ERROR_ROLL_STRING_EMPTY; // value +extern NSString * const SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS; // value + +// Terms of type SA_DB_TERM_TYPE_OPERATION (a.k.a. operation expressions) have a +// value for the SA_DB_OPERATOR key, which may be any of the allowed operators +// listed below. +// Operation expressions also have a value for the SA_DB_OPERAND_RIGHT key, +// and, possibly, a value for the SA_DB_OPERAND_LEFT key as well. +// (An operation expression with an SA_DB_OPERATOR value of SA_DB_OPERATOR_MINUS +// that does not have an SA_DB_OPERAND_LEFT value is simply a negation of the +// SA_DB_OPERAND_RIGHT value.) +// The values for the SA_DB_OPERAND_RIGHT (and, if present, SA_DB_OPERAND_LEFT) +// are themselves expressions of some type (i.e. NSDictionary objects), which +// must be recursively evaluated. +extern NSString * const SA_DB_OPERATOR; // key +extern NSString * const SA_DB_OPERATOR_MINUS; // value +extern NSString * const SA_DB_OPERATOR_PLUS; // value +extern NSString * const SA_DB_OPERATOR_TIMES; // value +extern NSString * const SA_DB_OPERAND_LEFT; // key +extern NSString * const SA_DB_OPERAND_RIGHT; // key + +// Terms of type SA_DB_TERM_TYPE_ROLL_COMMAND (a.k.a. roll command expressions) +// have a value for the SA_DB_ROLL_COMMAND key, which may be any of the roll +// commands listed below. +// Roll command expressions also have values for the keys SA_DB_ROLL_DIE_COUNT +// and SA_DB_ROLL_DIE_SIZE, which are themselves expressions of some type +// (i.e. NSDictionary objects), which must be recursively evaluated. +extern NSString * const SA_DB_ROLL_COMMAND; // key +extern NSString * const SA_DB_ROLL_COMMAND_SUM; // value +extern NSString * const SA_DB_ROLL_DIE_COUNT; // key +extern NSString * const SA_DB_ROLL_DIE_SIZE; // key + +// Terms of type SA_DB_TERM_TYPE_VALUE (a.k.a. simple value expressions) have a +// value for the SA_DB_VALUE key, which is an NSNumber that represents an +// NSInteger value. +// NOTE: Despite being an NSInteger, this numeric value may not be negative. +extern NSString * const SA_DB_VALUE; // key + +// All terms that were generated via parsing a string should have a value for +// the SA_DB_INPUT_STRING key. This is in order to be able to reconstruct the +// input, for useful output later (i.e., it's used by the results formatter). +// The value is, of course, an NSString. +// Optionally, there may also be a value for the SA_DB_ATTRIBUTED_INPUT_STRING +// key. The current implementation does not set a value for this key, nor is it +// used in any way, but it may be used in future versions or in alternate +// implementations. +extern NSString * const SA_DB_INPUT_STRING; // key +extern NSString * const SA_DB_ATTRIBUTED_INPUT_STRING; // key + +/******************************************************/ +#pragma mark String constants for expression evaluation +/******************************************************/ + +// Additional values for key SA_DB_ERROR +extern NSString * const SA_DB_ERROR_UNKNOWN_ROLL_COMMAND; // value +extern NSString * const SA_DB_ERROR_DIE_COUNT_NEGATIVE; // value +extern NSString * const SA_DB_ERROR_DIE_COUNT_EXCESSIVE; // value +extern NSString * const SA_DB_ERROR_DIE_SIZE_INVALID; // value +extern NSString * const SA_DB_ERROR_DIE_SIZE_EXCESSIVE; // value +extern NSString * const SA_DB_ERROR_UNKNOWN_OPERATOR; // value +extern NSString * const SA_DB_ERROR_INVALID_EXPRESSION; // value +extern NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_NEGATION; // value +extern NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_ADDITION; // value +extern NSString * const SA_DB_ERROR_INTEGER_UNDERFLOW_ADDITION; // value +extern NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_SUBTRACTION; // value +extern NSString * const SA_DB_ERROR_INTEGER_UNDERFLOW_SUBTRACTION; // value +extern NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_MULTIPLICATION; // value +extern NSString * const SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION; // value + +// Successfully evaluated terms (i.e., those that have no errors) have a value +// for the SA_DB_RESULT key. This value is an NSNumber that represents an +// NSInteger value. +extern NSString * const SA_DB_RESULT; // key + +// Successfully evaluated roll command terms (i.e. those that have no errors) +// have a value for the SA_DB_ROLLS key. This is an NSArray containing all of +// the individual die rolls that were generated by executing the roll command. +extern NSString * const SA_DB_ROLLS; // key + +/***************************************************************/ +#pragma mark String constants for retrieving string format rules +/***************************************************************/ + +extern NSString * const SA_DB_STRING_FORMAT_RULES_PLIST_NAME; + +/* + The string format rules file (whose filename - minus the .plist extension - + is given by the SA_DB_STRING_FORMAT_RULES_PLIST_NAME string) contains + values for variables that define the properties of legal die roll strings, + as well as certain variables that define the format of result strings. + + The file is organized as a dictionary contaning several sub-dictionaries. The + valid keys (those for which values are present in the file), and the values + that those keys may be expected to have, are listed below. + */ + +// The value for the top-level key SA_DB_VALID_CHARACTERS is a dictionary that +// defines those characters that are recognized as valid representations of +// various components of a die roll string. This dictionary has values for the +// keys listed below whose names begin with SA_DB_VALID_. +extern NSString * const SA_DB_VALID_CHARACTERS; + +// The value for the key SA_DB_VALID_NUMERAL_CHARACTERS is a string that +// contains all characters that are recognized as representing decimal numerals. +// (Usually, this will be the 10 characters representing the standard Arabic +// numerals.) +extern NSString * const SA_DB_VALID_NUMERAL_CHARACTERS; + +// The value for the key SA_DB_VALID_OPERATOR_CHARACTERS is a dictionary that +// defines those characters that are recognized as valid representations of +// the supported mathematical operators. This dictionary has values for keys +// corresponding to the names of each of the supported operators (see +// the "String constants for expression parsing" section, above). +extern NSString * const SA_DB_VALID_OPERATOR_CHARACTERS; + +// The value for the key SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS is a +// string that contains all the characters that are recognized as representing +// die roll command delimiters. (Usually this is the lowercase and uppercase +// versions of the letter 'd', as in '1d20' or '4D6'.) +extern NSString * const SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS; + +// The value for the top-level key SA_DB_CANONICAL_REPRESENTATIONS is a +// dictionary that defines canonical representations of certain components +// of a formatted die roll string. This dictionary has values for the keys +// listed below whose names begin with SA_DB_CANONICAL_. +extern NSString * const SA_DB_CANONICAL_REPRESENTATIONS; + +// The value for the key SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS is a +// dictionary that defines canonical representations of each of the +// supported mathematical operators (for use when formatting results for +// output). This dictionary has values for keys corresponding to the names of +// each of the supported operators (see the "String constants for expression +// parsing" section, above). +extern NSString * const SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS; + +// The value for the key SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION +// is the canonical representation of the die roll command delimiter. +extern NSString * const SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION; \ No newline at end of file diff --git a/SA_DiceExpressionStringConstants.m b/SA_DiceExpressionStringConstants.m new file mode 100644 index 0000000..9547f35 --- /dev/null +++ b/SA_DiceExpressionStringConstants.m @@ -0,0 +1,84 @@ +// +// SA_DiceExpressionStringConstants.m +// RPGBot +// +// Created by Sandy Achmiz on 1/2/16. +// +// + +#import "SA_DiceExpressionStringConstants.h" + +/***************************************************/ +#pragma mark String constants for expression parsing +/***************************************************/ + +NSString * const SA_DB_TERM_TYPE = @"SA_DB_TERM_TYPE"; +NSString * const SA_DB_TERM_TYPE_VALUE = @"SA_DB_TERM_TYPE_VALUE"; +NSString * const SA_DB_TERM_TYPE_ROLL_COMMAND = @"SA_DB_TERM_TYPE_ROLL_COMMAND"; +NSString * const SA_DB_TERM_TYPE_OPERATION = @"SA_DB_TERM_TYPE_OPERATION"; +NSString * const SA_DB_TERM_TYPE_NONE = @"SA_DB_TERM_TYPE_NONE"; + +NSString * const SA_DB_ERRORS = @"SA_DB_ERRORS"; +NSString * const SA_DB_ERROR_ROLL_STRING_EMPTY = @"SA_DB_ERROR_ROLL_STRING_EMPTY"; +NSString * const SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS = @"SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS"; + +NSString * const SA_DB_OPERATOR = @"SA_DB_OPERATOR"; +NSString * const SA_DB_OPERATOR_MINUS = @"SA_DB_OPERATOR_MINUS"; +NSString * const SA_DB_OPERATOR_PLUS = @"SA_DB_OPERATOR_PLUS"; +NSString * const SA_DB_OPERATOR_TIMES = @"SA_DB_OPERATOR_TIMES"; +NSString * const SA_DB_OPERAND_LEFT = @"SA_DB_OPERAND_LEFT"; +NSString * const SA_DB_OPERAND_RIGHT = @"SA_DB_OPERAND_RIGHT"; + +NSString * const SA_DB_ROLL_COMMAND = @"SA_DB_ROLL_COMMAND"; +NSString * const SA_DB_ROLL_COMMAND_SUM = @"SA_DB_ROLL_COMMAND_SUM"; +NSString * const SA_DB_ROLL_DIE_COUNT = @"SA_DB_ROLL_DIE_COUNT"; +NSString * const SA_DB_ROLL_DIE_SIZE = @"SA_DB_ROLL_DIE_SIZE"; + +NSString * const SA_DB_VALUE = @"SA_DB_VALUE"; + +NSString * const SA_DB_INPUT_STRING = @"SA_DB_INPUT_STRING"; +NSString * const SA_DB_ATTRIBUTED_INPUT_STRING = @"SA_DB_ATTRIBUTED_INPUT_STRING"; + +/******************************************************/ +#pragma mark String constants for expression evaluation +/******************************************************/ + +NSString * const SA_DB_ERROR_UNKNOWN_ROLL_COMMAND = @"SA_DB_ERROR_UNKNOWN_ROLL_COMMAND"; +NSString * const SA_DB_ERROR_DIE_COUNT_NEGATIVE = @"SA_DB_ERROR_DIE_COUNT_NEGATIVE"; +NSString * const SA_DB_ERROR_DIE_COUNT_EXCESSIVE = @"SA_DB_ERROR_DIE_COUNT_EXCESSIVE"; +NSString * const SA_DB_ERROR_DIE_SIZE_INVALID = @"SA_DB_ERROR_DIE_SIZE_INVALID"; +NSString * const SA_DB_ERROR_DIE_SIZE_EXCESSIVE = @"SA_DB_ERROR_DIE_SIZE_EXCESSIVE"; +NSString * const SA_DB_ERROR_UNKNOWN_OPERATOR = @"SA_DB_ERROR_UNKNOWN_OPERATOR"; +NSString * const SA_DB_ERROR_INVALID_EXPRESSION = @"SA_DB_ERROR_INVALID_EXPRESSION"; +NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_NEGATION = @"SA_DB_ERROR_INTEGER_OVERFLOW_NEGATION"; +NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_ADDITION = @"SA_DB_ERROR_INTEGER_OVERFLOW_ADDITION"; +NSString * const SA_DB_ERROR_INTEGER_UNDERFLOW_ADDITION = @"SA_DB_ERROR_INTEGER_UNDERFLOW_ADDITION"; +NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_SUBTRACTION = @"SA_DB_ERROR_INTEGER_OVERFLOW_SUBTRACTION"; +NSString * const SA_DB_ERROR_INTEGER_UNDERFLOW_SUBTRACTION = @"SA_DB_ERROR_INTEGER_UNDERFLOW_SUBTRACTION"; +NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_MULTIPLICATION = @"SA_DB_ERROR_INTEGER_OVERFLOW_MULTIPLICATION"; +NSString * const SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION = @"SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION"; + +NSString * const SA_DB_RESULT = @"SA_DB_RESULT"; + +NSString * const SA_DB_ROLLS = @"SA_DB_ROLLS"; + +/******************************************************/ +#pragma mark String constants for expression formatting +/******************************************************/ + +NSString * const SA_DB_LABEL = @"SA_DB_LABEL"; + +/***************************************************************/ +#pragma mark String constants for retrieving string format rules +/***************************************************************/ + +NSString * const SA_DB_STRING_FORMAT_RULES_PLIST_NAME = @"SA_DB_StringFormatRules"; + +NSString * const SA_DB_VALID_CHARACTERS = @"SA_DB_VALID_CHARACTERS"; +NSString * const SA_DB_VALID_NUMERAL_CHARACTERS = @"SA_DB_VALID_NUMERAL_CHARACTERS"; +NSString * const SA_DB_VALID_OPERATOR_CHARACTERS = @"SA_DB_VALID_OPERATOR_CHARACTERS"; +NSString * const SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS = @"SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS"; + +NSString * const SA_DB_CANONICAL_REPRESENTATIONS = @"SA_DB_CANONICAL_REPRESENTATIONS"; +NSString * const SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS = @"SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS"; +NSString * const SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION = @"SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION"; diff --git a/SA_DiceFormatter.h b/SA_DiceFormatter.h new file mode 100644 index 0000000..b07bb8f --- /dev/null +++ b/SA_DiceFormatter.h @@ -0,0 +1,156 @@ +// +// SA_DiceFormatter.h +// RPGBot +// +// Created by Sandy Achmiz on 12/31/15. +// +// + +#import + +/*********************/ +#pragma mark Constants +/*********************/ +/* + These constants describe one of several behavior modes for die string results + formatting. + + NOTE: Each of the modes have their own set of settings, which may be configured + via various of the SA_DiceFormatter properties. Be sure to use the + appropriate settings for the currently set behavior mode. + + NOTE 2: SA_DiceParser also has a behavior mode property, and can operate in + one of several parser behavior modes. Each formatter behavior mode is + appropriate for formatting results generated by some parser behavior modes but + not others. Attempting to format a results tree in a formatter mode that does + not support the parser mode in which those results were generated causes + undefined behavior. Thus, when using any a formatter and a parser together, we + must always make sure that the two objects are set to operate in compatible + behavior modes. + + Each formatter behavior mode is described below. Each description also lists + the parser modes which are supported by that formatter mode. + + ====================== + ==== DEFAULT mode ==== + ====================== + + "Default" mode is an alias for whatever default behavior is currently set for + new SA_DiceFormatter instances. (The "default default" behavior for the + current implementation is "legacy".) + + ===================== + ==== LEGACY mode ==== + ===================== + + Legacy mode mostly emulates the output format of DiceBot by Sabin (and Dawn by + xthemage before it). (It adds optional error reporting, and several minor + aesthetic enhancement options.) + + The legacy mode output format reproduces the entire die roll expression, in + an expanded form (with whitespace inserted between operators and operands); + prints each individual die roll (grouped by roll command), along with the + sum of each die group. The final result is also printed, as is a label + (if any). Typical legacy mode output looks like this: + + (Hasan_the_Great) 4d6 < 3 3 4 6 = 16 > + 2d4 < 2 4 = 6 > + 3 + 20 = 45 + + This output line would be generated by a roll string as follows: + + 4d6+2d4+3+20;Hasan_the_Great + + Here are some typical output strings for erroneous or malformed input strings. + + INPUT: + 8d6+4d0+5 + + OUTPUT: + 8d6 < 3 1 2 5 2 2 1 6 = 22 > + 4d0 < ERROR > [ERROR: Invalid die size (zero or negative)] + + Legacy mode does not support attributed text of any kind. + + SUPPORTED PARSER MODES: Legacy. + + ===================== + ==== SIMPLE mode ==== + ===================== + + Simple mode generates a very minimal output format. It prints the final result + of a die roll expression, or the word 'ERROR' if any error occurred. It also + prints the label (if any). + + SUPPORTED PARSER MODES: Feepbot, Legacy. + + ===================== + ==== MODERN mode ==== + ===================== + + >>> NOT YET IMPLEMENTED <<< + + Modern mode supports a comprehensive range of commands and capabilities, and + has many configurable options for extensive customizability of the output + format. It also supports attributed text. + + SUPPORTED PARSER MODES: Feepbot, Legacy, Modern. + + ====================== + ==== FEEPBOT mode ==== + ====================== + + >>> NOT YET IMPLEMENTED <<< + + Feepbot mode emulates the output format of feepbot by feep. + + SUPPORTED PARSER MODES: Feepbot, Legacy. + */ +typedef enum +{ + SA_DiceFormatterBehaviorDefault = 0, + SA_DiceFormatterBehaviorSimple = 1, + SA_DiceFormatterBehaviorLegacy = 1337, + SA_DiceFormatterBehaviorModern = 2001, + SA_DiceFormatterBehaviorFeepbot = 65516 +} SA_DiceFormatterBehavior; + +/************************************************/ +#pragma mark - SA_DiceFormatter class declaration +/************************************************/ + +@interface SA_DiceFormatter : NSObject + +/**********************************/ +#pragma mark - Properties (general) +/**********************************/ + +@property SA_DiceFormatterBehavior formatterBehavior; + +/*************************************************/ +#pragma mark - Properties ("legacy" behavior mode) +/*************************************************/ + +@property BOOL legacyModeErrorReportingEnabled; + +/****************************************/ +#pragma mark - "Class property" accessors +/****************************************/ + ++ (void)setDefaultFormatterBehavior:(SA_DiceFormatterBehavior)defaultFormatterBehavior; ++ (SA_DiceFormatterBehavior)defaultFormatterBehavior; + +/********************************************/ +#pragma mark - Initializers & factory methods +/********************************************/ + +- (instancetype)init; +- (instancetype)initWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior NS_DESIGNATED_INITIALIZER; ++ (instancetype)defaultFormatter; ++ (instancetype)formatterWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior; + +/****************************/ +#pragma mark - Public methods +/****************************/ + +- (NSString *)stringFromExpression:(NSDictionary *)expression; +- (NSAttributedString *)attributedStringFromExpression:(NSDictionary *)expression; + +@end diff --git a/SA_DiceFormatter.m b/SA_DiceFormatter.m new file mode 100644 index 0000000..4531a1a --- /dev/null +++ b/SA_DiceFormatter.m @@ -0,0 +1,447 @@ +// +// SA_DiceFormatter.m +// RPGBot +// +// Created by Sandy Achmiz on 12/31/15. +// +// + +#import "SA_DiceFormatter.h" + +#import "SA_DiceExpressionStringConstants.h" +#import "SA_DiceErrorHandling.h" + +/********************************/ +#pragma mark File-scope variables +/********************************/ + +static SA_DiceFormatterBehavior _defaultFormatterBehavior = SA_DiceFormatterBehaviorLegacy; +static NSDictionary *_errorDescriptions; +static NSDictionary *_stringFormatRules; + +/**********************************************************/ +#pragma mark - SA_DiceFormatter class implementation +/**********************************************************/ + +@implementation SA_DiceFormatter +{ + SA_DiceFormatterBehavior _formatterBehavior; +} + +/**********************************/ +#pragma mark - Properties (general) +/**********************************/ + +- (void)setFormatterBehavior:(SA_DiceFormatterBehavior)newFormatterBehavior +{ + _formatterBehavior = newFormatterBehavior; + + switch (_formatterBehavior) + { + case SA_DiceFormatterBehaviorLegacy: + self.legacyModeErrorReportingEnabled = YES; + break; + + case SA_DiceFormatterBehaviorSimple: + case SA_DiceFormatterBehaviorModern: + case SA_DiceFormatterBehaviorFeepbot: + break; + + case SA_DiceFormatterBehaviorDefault: + default: + [self setFormatterBehavior:[SA_DiceFormatter defaultFormatterBehavior]]; + break; + } +} + +- (SA_DiceFormatterBehavior)formatterBehavior +{ + return _formatterBehavior; +} + +/****************************************/ +#pragma mark - "Class property" accessors +/****************************************/ + ++ (void)setDefaultFormatterBehavior:(SA_DiceFormatterBehavior)newDefaultFormatterBehavior +{ + if(newDefaultFormatterBehavior == SA_DiceFormatterBehaviorDefault) + { + _defaultFormatterBehavior = SA_DiceFormatterBehaviorLegacy; + } + else + { + _defaultFormatterBehavior = newDefaultFormatterBehavior; + } +} + ++ (SA_DiceFormatterBehavior)defaultFormatterBehavior +{ + return _defaultFormatterBehavior; +} + ++ (NSDictionary *)stringFormatRules +{ + if(_stringFormatRules == nil) + { + [SA_DiceFormatter loadStringFormatRules]; + } + + return _stringFormatRules; +} + +/********************************************/ +#pragma mark - Initializers & factory methods +/********************************************/ + +- (instancetype)init +{ + return [self initWithBehavior:SA_DiceFormatterBehaviorDefault]; +} + +- (instancetype)initWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior +{ + if(self = [super init]) + { + self.formatterBehavior = formatterBehavior; + + if(_errorDescriptions == nil) + { + [SA_DiceFormatter loadErrorDescriptions]; + } + if(_stringFormatRules == nil) + { + [SA_DiceFormatter loadStringFormatRules]; + } + } + return self; +} + ++ (instancetype)defaultFormatter +{ + return [[SA_DiceFormatter alloc] initWithBehavior:SA_DiceFormatterBehaviorDefault]; +} + ++ (instancetype)formatterWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior +{ + return [[SA_DiceFormatter alloc] initWithBehavior:formatterBehavior]; +} + +/****************************/ +#pragma mark - Public methods +/****************************/ + +- (NSString *)stringFromExpression:(NSDictionary *)expression +{ + if(_formatterBehavior == SA_DiceFormatterBehaviorSimple) + { + return [self simpleStringFromExpression:expression]; + } + else // if(_formatterBehavior == SA_DiceFormatterBehaviorLegacy) + { + return [self legacyStringFromExpression:expression]; + } +} + +// NOT YET IMPLEMENTED +- (NSAttributedString *)attributedStringFromExpression:(NSDictionary *)expression +{ + return [[NSAttributedString alloc] initWithString:[self stringFromExpression:expression]]; +} + +/**********************************************/ +#pragma mark - "Legacy" behavior implementation +/**********************************************/ + +// PROPERTIES + +@synthesize legacyModeErrorReportingEnabled = _legacyModeErrorReportingEnabled; + +// METHODS + +- (NSString *)legacyStringFromExpression:(NSDictionary *)expression +{ + __block NSMutableString *formattedString = [NSMutableString string]; + + // Attach the formatted string representation of the expression itself. + [formattedString appendString:[self legacyStringFromIntermediaryExpression:expression]]; + + // An expression may contain either a result, or one or more errors. + // If a result is present, attach it. If errors are present, attach them + // only if error reporting is enabled. + if(expression[SA_DB_RESULT] != nil) + { + [formattedString appendFormat:@" = %@", expression[SA_DB_RESULT]]; + } + else if(_legacyModeErrorReportingEnabled == YES && [getErrorsForExpression(expression) count] > 1) + { + if([getErrorsForExpression(expression) count] == 1) + { + [formattedString appendFormat:@" [ERROR: %@]", [SA_DiceFormatter descriptionForError:[getErrorsForExpression(expression) firstObject]]]; + } + else + { + [formattedString appendFormat:@" [ERRORS: "]; + [getErrorsForExpression(expression) enumerateObjectsUsingBlock:^(NSString *error, NSUInteger idx, BOOL *stop) + { + [formattedString appendString:[SA_DiceFormatter descriptionForError:error]]; + if(idx != [getErrorsForExpression(expression) count] - 1) + { + [formattedString appendFormat:@", "]; + } + else + { + [formattedString appendFormat:@"]"]; + } + }]; + } + } + + return formattedString; +} + +- (NSString *)legacyStringFromIntermediaryExpression:(NSDictionary *)expression +{ + /* + In legacy behavior, we do not print the results of intermediate terms in + the expression tree (since the legacy output format was designed for + expressions generated by a parser that does not support parentheses, + doing so would not make sense anyway). + + The exception is roll commands, where the result of a roll-and-sum command + is printed along with the rolls. + + For this reasons, when we recursively retrieve the string representations + of sub-expressions, we call this method, not legacyStringFromExpression:. + */ + + if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_OPERATION]) + { + return [self legacyStringFromOperationExpression:expression]; + } + else if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_ROLL_COMMAND]) + { + return [self legacyStringFromRollCommandExpression:expression]; + } + else if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_VALUE]) + { + return [self legacyStringFromValueExpression:expression]; + } + else // if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_NONE]), probably + { + return expression[SA_DB_INPUT_STRING]; + } +} + +- (NSString *)legacyStringFromOperationExpression:(NSDictionary *)expression +{ + NSMutableString *formattedString = [NSMutableString string]; + + // Check to see if the term is a negation or subtraction operation. + if([expression[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_MINUS]) + { + // Get the canonical representation for the operator. + NSString *operatorString = [SA_DiceFormatter canonicalRepresentationForOperator:SA_DB_OPERATOR_MINUS]; + + // If we have a left operand, it's subtraction. If we do not, it's + // negation. + if(expression[SA_DB_OPERAND_LEFT] == nil) + { + // Get the operand. + NSDictionary *rightOperandExpression = expression[SA_DB_OPERAND_RIGHT]; + + // Write out the string representations of operator and the + // right-hand-side expression. + [formattedString appendString:operatorString]; + [formattedString appendString:[self legacyStringFromIntermediaryExpression:rightOperandExpression]]; + + return formattedString; + } + else + { + // Get the operands. + NSDictionary *leftOperandExpression = expression[SA_DB_OPERAND_LEFT]; + NSDictionary *rightOperandExpression = expression[SA_DB_OPERAND_RIGHT]; + + // Write out the string representations of the left-hand-side + // expression, the operator, and the right-hand-side expression. + [formattedString appendString:[self legacyStringFromIntermediaryExpression:leftOperandExpression]]; + [formattedString appendFormat:@" %@ ", operatorString]; + [formattedString appendString:[self legacyStringFromIntermediaryExpression:rightOperandExpression]]; + + return formattedString; + } + } + // Check to see if the term is an addition or subtraction operation. + else if([expression[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_PLUS]) + { + NSString *operatorString = [SA_DiceFormatter canonicalRepresentationForOperator:SA_DB_OPERATOR_PLUS]; + + // Get the operands. + NSDictionary *leftOperandExpression = expression[SA_DB_OPERAND_LEFT]; + NSDictionary *rightOperandExpression = expression[SA_DB_OPERAND_RIGHT]; + + // Write out the string representations of the left-hand-side + // expression, the operator, and the right-hand-side expression. + [formattedString appendString:[self legacyStringFromIntermediaryExpression:leftOperandExpression]]; + [formattedString appendFormat:@" %@ ", operatorString]; + [formattedString appendString:[self legacyStringFromIntermediaryExpression:rightOperandExpression]]; + + return formattedString; + } + // Check to see if the term is a multiplication operation. + else if([expression[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_TIMES]) + { + // Get the canonical representation for the operator. + NSString *operatorString = [SA_DiceFormatter canonicalRepresentationForOperator:SA_DB_OPERATOR_TIMES]; + + // Get the operands. + NSDictionary *leftOperandExpression = expression[SA_DB_OPERAND_LEFT]; + NSDictionary *rightOperandExpression = expression[SA_DB_OPERAND_RIGHT]; + + // Write out the string representations of the left-hand-side + // expression, the operator, and the right-hand-side expression. + [formattedString appendString:[self legacyStringFromIntermediaryExpression:leftOperandExpression]]; + [formattedString appendFormat:@" %@ ", operatorString]; + [formattedString appendString:[self legacyStringFromIntermediaryExpression:rightOperandExpression]]; + + return formattedString; + } + else + { + // If the operator is not one of the supported operators, default to + // outputting the input string. + return expression[SA_DB_INPUT_STRING]; + } +} + +- (NSString *)legacyStringFromRollCommandExpression:(NSDictionary *)expression +{ + /* + In legacy behavior, we print the result of roll commands with the rolls + generated by the roll command. If a roll command generates a roll-related + error (any of the errors that begin with SA_DB_DIE_), we print "ERROR" + in place of a result. + + Legacy behavior assumes support for roll-and-sum only, so we do not need + to adjust the output format for different roll commands. + */ + __block NSMutableString *formattedString = [NSMutableString string]; + + // Append the die roll expression itself. + [formattedString appendString:[self legacyStringFromIntermediaryExpression:expression[SA_DB_ROLL_DIE_COUNT]]]; + [formattedString appendString:[SA_DiceFormatter canonicalRollCommandDelimiterRepresentation]]; + [formattedString appendString:[self legacyStringFromIntermediaryExpression:expression[SA_DB_ROLL_DIE_SIZE]]]; + + [formattedString appendFormat:@" < "]; + + // Append a list of the rolled values, if any. + if(expression[SA_DB_ROLLS] != nil && [expression[SA_DB_ROLLS] count] > 0) + { + [expression[SA_DB_ROLLS] enumerateObjectsUsingBlock:^(NSNumber *roll, NSUInteger idx, BOOL *stop) { + [formattedString appendFormat:@"%@ ", roll]; + }]; + + [formattedString appendFormat:@"= "]; + } + + // Append either the result, or the word 'ERROR'. + [formattedString appendFormat:@"%@ >", ((expression[SA_DB_RESULT] != nil) ? expression[SA_DB_RESULT] : @"ERROR")]; + + return formattedString; +} + +- (NSString *)legacyStringFromValueExpression:(NSDictionary *)expression +{ + // We use the value for the SA_DB_VALUE key and not the SA_DB_RESULT key + // because they should be the same, and the SA_DB_RESULT key might not + // have a value (if the expression was not evaluated); this saves us + // having to compare it against nil, and saves code. + return [expression[SA_DB_VALUE] stringValue]; +} + +/**********************************************/ +#pragma mark - "Simple" behavior implementation +/**********************************************/ + +- (NSString *)simpleStringFromExpression:(NSDictionary *)expression +{ + NSMutableString *formattedString = [NSMutableString string]; + + if(expression[SA_DB_RESULT] != nil) + { + [formattedString appendFormat:@"%@", expression[SA_DB_RESULT]]; + } + else + { + [formattedString appendFormat:@"ERROR"]; + } + + return formattedString; +} + +/****************************/ +#pragma mark - Helper methods +/****************************/ + ++ (void)loadErrorDescriptions +{ + NSString* errorDescriptionsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"SA_DB_ErrorDescriptions" ofType:@"plist"]; + _errorDescriptions = [NSDictionary dictionaryWithContentsOfFile:errorDescriptionsPath]; + if(_errorDescriptions) + { + NSLog(@"Error descriptions loaded successfully."); + } + else + { + NSLog(@"Could not load error descriptions!"); + } +} + ++ (NSString *)descriptionForError:(NSString *)error +{ + if(_errorDescriptions == nil) + { + [SA_DiceFormatter loadErrorDescriptions]; + } + + if(_errorDescriptions[error] != nil) + { + return _errorDescriptions[error]; + } + else + { + return error; + } +} + ++ (void)loadStringFormatRules +{ + NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME ofType:@"plist"]; + _stringFormatRules = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath]; + if(_stringFormatRules) + { + NSLog(@"String format rules loaded successfully."); + } + else + { + NSLog(@"Could not load string format rules!"); + } +} + ++ (NSString *)canonicalRepresentationForOperator:(NSString *)operatorName +{ + return [SA_DiceFormatter canonicalOperatorRepresentations][operatorName]; +} + ++ (NSDictionary *)canonicalOperatorRepresentations +{ + return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS]; +} + ++ (NSString *)canonicalRollCommandDelimiterRepresentation +{ + return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION]; +} + +@end diff --git a/SA_DiceParser.h b/SA_DiceParser.h new file mode 100644 index 0000000..23f180c --- /dev/null +++ b/SA_DiceParser.h @@ -0,0 +1,155 @@ +// +// SA_DiceParser.h +// RPGBot +// +// Created by Sandy Achmiz on 12/30/15. +// +// + +#import + +/*********************/ +#pragma mark Constants +/*********************/ +/* + These constants describe one of several behavior modes for roll string parsing. + + Each behavior mode defines a set of capabilities - what sorts of expressions, + operators, roll commands, etc. the parser recognizes in that mode, and the + syntax for using those capabilities. + + The structure and content of the expression tree generated by the parser from + a given die roll string also depends on the behavior mode that the parser is + set to. + + NOTE: While SA_DiceEvaluator is modeless (it correctly evaluates all + expressions generated by the parser in any supported mode), + SA_DiceFormatter is modal. SA_DiceFormatter provides several + formatter behaviors, each of which supports one or more parser modes. Using the + wrong formatter behavior mode for an expression tree (that is, passing an + SA_DiceFormatter instance an expression tree that was was generated + by a parser mode that is not supported by the formatter's currently set + formatter behavior mode) results in undefined behavior. + + See SA_DiceFormatter.h for a list of which formatter behavior modes are + appropriate for use with which parser modes. See also SA_DiceBot.h for a + discussion of how the currently set parser mode affects what die-roll-related + bot commands are available. (Together, the parser behavior mode and the results + formatter behavior mode define the behavior and capabilities of an SA_DiceBot.) + + Each mode is described below. + + ====================== + ==== DEFAULT mode ==== + ====================== + + "Default" mode is an alias for whatever default behavior is currently set for + new SA_DiceParser instances. (The "default default" behavior for the current + implementation is "legacy".) + + ===================== + ==== LEGACY mode ==== + ===================== + + Legacy mode (mostly) emulates DiceBot by Sabin (and Dawn by xthemage before + it). It replicates the parsing and evaluation functions of those venerable + bots, providing the following capabilities: + + 1. Arithmetic operations: addition, subtraction, multiplication. Syntax is as + normal for such operations, e.g.: + + 2+3 + 25*10 + 5-3*2 + + Normal operator precedence and behavior (commutativity, associativity) apply. + + 2. Simple roll-and-sum. Roll X dice, each with Y sides, and take the sum of + the rolled values, by inputting 'XdY' where X is a nonnegative integer and Y + is a positive integer, e.g.: + + 1d20 + 5d6 + 8d27 + + 3. Left-associative recursive roll-and-sum. Roll X dice, each with Y sides, + and take the sum of the rolled values, by inputting 'XdY', where Y is a + positive integer and X may be a nonnegative integer or a recursive roll-and-sum + expression, e.g.: + + 5d6d10 + 1d20d6 + 4d4d4d4d4d4d4 + + The above capabilities may be used indiscriminately in a single roll string: + + 1d20-5 + 5d6*10 + 1d20+2d4*10 + 5d6d10-2*3 + 5+3-2*4d6+2d10d3-20+5d4*2 + + NOTE: The 'd' operator takes precedence over arithmetic operators. (Legacy + mode does not support parentheses.) + + NOTE 2: Legacy mode does not support whitespace within roll strings. + + ===================== + ==== MODERN mode ==== + ===================== + + >>> NOT YET IMPLEMENTED <<< + + Modern mode provides a comprehensive range of commands and capabilities. + + ====================== + ==== FEEPBOT mode ==== + ====================== + + >>> NOT YET IMPLEMENTED <<< + + Feepbot mode emulates feepbot by feep. + */ +typedef enum +{ + SA_DiceParserBehaviorDefault = 0, + SA_DiceParserBehaviorLegacy = 1337, + SA_DiceParserBehaviorModern = 2001, + SA_DiceParserBehaviorFeepbot = 65516 +} SA_DiceParserBehavior; + +/*********************************************/ +#pragma mark - SA_DiceParser class declaration +/*********************************************/ + +@interface SA_DiceParser : NSObject + +/************************/ +#pragma mark - Properties +/************************/ + +@property SA_DiceParserBehavior parserBehavior; + +/****************************************/ +#pragma mark - "Class property" accessors +/****************************************/ + ++ (void)setDefaultParserBehavior:(SA_DiceParserBehavior)defaultParserBehavior; ++ (SA_DiceParserBehavior)defaultParserBehavior; + +/********************************************/ +#pragma mark - Initializers & factory methods +/********************************************/ + +- (instancetype)init; +- (instancetype)initWithBehavior:(SA_DiceParserBehavior)parserBehavior NS_DESIGNATED_INITIALIZER; ++ (instancetype)defaultParser; ++ (instancetype)parserWithBehavior:(SA_DiceParserBehavior)parserBehavior; + +/****************************/ +#pragma mark - Public methods +/****************************/ + +- (NSDictionary *)expressionForString:(NSString *)dieRollString; + +@end diff --git a/SA_DiceParser.m b/SA_DiceParser.m new file mode 100644 index 0000000..99d9626 --- /dev/null +++ b/SA_DiceParser.m @@ -0,0 +1,413 @@ +// +// SA_DiceParser.m +// RPGBot +// +// Created by Sandy Achmiz on 12/30/15. +// +// + +#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(@"Valid characters dictionary loaded successfully."); + } + else + { + 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