| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |||||
| <plist version="1.0"> | |||||
| <dict> | |||||
| <key>SA_DB_ERROR_ROLL_STRING_EMPTY</key> | |||||
| <string>Empty expression</string> | |||||
| <key>SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS</key> | |||||
| <string>Illegal characters detected</string> | |||||
| <key>SA_DB_ERROR_UNKNOWN_ROLL_COMMAND</key> | |||||
| <string>Unknown roll command</string> | |||||
| <key>SA_DB_ERROR_DIE_COUNT_NEGATIVE</key> | |||||
| <string>Die count negative</string> | |||||
| <key>SA_DB_ERROR_DIE_COUNT_EXCESSIVE</key> | |||||
| <string>Too many dice</string> | |||||
| <key>SA_DB_ERROR_DIE_SIZE_INVALID</key> | |||||
| <string>Invalid die size (zero or negative)</string> | |||||
| <key>SA_DB_ERROR_DIE_SIZE_EXCESSIVE</key> | |||||
| <string>Die size too big</string> | |||||
| <key>SA_DB_ERROR_UNKNOWN_OPERATOR</key> | |||||
| <string>Unknown operator</string> | |||||
| <key>SA_DB_ERROR_INVALID_EXPRESSION</key> | |||||
| <string>Invalid expression</string> | |||||
| <key>SA_DB_ERROR_INTEGER_OVERFLOW_NEGATION</key> | |||||
| <string>Integer overflow during negation</string> | |||||
| <key>SA_DB_ERROR_INTEGER_OVERFLOW_ADDITION</key> | |||||
| <string>Integer overflow during addition</string> | |||||
| <key>SA_DB_ERROR_INTEGER_UNDERFLOW_ADDITION</key> | |||||
| <string>Integer underflow during addition</string> | |||||
| <key>SA_DB_ERROR_INTEGER_OVERFLOW_SUBTRACTION</key> | |||||
| <string>Integer overflow during subtraction</string> | |||||
| <key>SA_DB_ERROR_INTEGER_UNDERFLOW_SUBTRACTION</key> | |||||
| <string>Integer underflow during subtraction</string> | |||||
| <key>SA_DB_ERROR_INTEGER_OVERFLOW_MULTIPLICATION</key> | |||||
| <string>Integer overflow during multiplication</string> | |||||
| <key>SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION</key> | |||||
| <string>Integer underflow during multiplication</string> | |||||
| </dict> | |||||
| </plist> |
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |||||
| <plist version="1.0"> | |||||
| <dict> | |||||
| <key>SA_DB_VALID_CHARACTERS</key> | |||||
| <dict> | |||||
| <key>SA_DB_VALID_NUMERAL_CHARACTERS</key> | |||||
| <string>0123456789</string> | |||||
| <key>SA_DB_VALID_OPERATOR_CHARACTERS</key> | |||||
| <dict> | |||||
| <key>SA_DB_OPERATOR_MINUS</key> | |||||
| <string>-–−</string> | |||||
| <key>SA_DB_OPERATOR_PLUS</key> | |||||
| <string>+</string> | |||||
| <key>SA_DB_OPERATOR_TIMES</key> | |||||
| <string>*×⋅·</string> | |||||
| </dict> | |||||
| <key>SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS</key> | |||||
| <string>dD</string> | |||||
| </dict> | |||||
| <key>SA_DB_CANONICAL_REPRESENTATIONS</key> | |||||
| <dict> | |||||
| <key>SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS</key> | |||||
| <dict> | |||||
| <key>SA_DB_OPERATOR_MINUS</key> | |||||
| <string>-</string> | |||||
| <key>SA_DB_OPERATOR_PLUS</key> | |||||
| <string>+</string> | |||||
| <key>SA_DB_OPERATOR_TIMES</key> | |||||
| <string>*</string> | |||||
| </dict> | |||||
| <key>SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION</key> | |||||
| <string>d</string> | |||||
| </dict> | |||||
| </dict> | |||||
| </plist> |
| // | |||||
| // SA_DiceBag.h | |||||
| // RPGBot | |||||
| // | |||||
| // Created by Sandy Achmiz on 12/30/15. | |||||
| // | |||||
| // | |||||
| #import <Foundation/Foundation.h> | |||||
| /****************************************/ | |||||
| #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 |
| // | |||||
| // 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 |
| // | |||||
| // 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 <Foundation/Foundation.h> | |||||
| #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 <NSDictionary *> *)repliesForCommandString:(NSString *)commandString messageInfo:(NSDictionary *)messageInfo byName:(BOOL)byName; | |||||
| - (void)loadDefaultCommandResponders; | |||||
| @end |
| // | |||||
| // 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 <NSDictionary *> *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 <NSDictionary *> *)repliesForCommandString:(NSString *)commandString messageInfo:(NSDictionary *)messageInfo byName:(BOOL)byName | |||||
| { | |||||
| NSError *error; | |||||
| NSArray <NSDictionary *> *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 |
| // | |||||
| // SA_DiceErrorHandling.h | |||||
| // RPGBot | |||||
| // | |||||
| // Created by Sandy Achmiz on 1/11/16. | |||||
| // | |||||
| // | |||||
| #ifndef SA_DiceErrorHandling_h | |||||
| #define SA_DiceErrorHandling_h | |||||
| #import <Foundation/Foundation.h> | |||||
| void addErrorToExpression (NSString *error, NSMutableDictionary *expression); | |||||
| void addErrorsFromExpressionToExpression (NSDictionary *sourceExpression, NSMutableDictionary *targetExpression); | |||||
| NSArray <NSString *> *getErrorsForExpression (NSDictionary *expression); | |||||
| #endif /* SA_DiceErrorHandling_h */ |
| // | |||||
| // 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 <NSString *> 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 <NSString *> arrayWithArray:sourceExpression[SA_DB_ERRORS]]; | |||||
| } | |||||
| else | |||||
| { | |||||
| [targetExpression[SA_DB_ERRORS] addObjectsFromArray:sourceExpression[SA_DB_ERRORS]]; | |||||
| } | |||||
| } | |||||
| NSArray <NSString *> *getErrorsForExpression (NSDictionary *expression) | |||||
| { | |||||
| return ([expression[SA_DB_ERRORS] count] > 0) ? expression[SA_DB_ERRORS] : nil; | |||||
| } |
| // | |||||
| // SA_DiceEvaluator.h | |||||
| // RPGBot | |||||
| // | |||||
| // Created by Sandy Achmiz on 1/2/16. | |||||
| // | |||||
| // | |||||
| #import <Foundation/Foundation.h> | |||||
| @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 |
| // | |||||
| // 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 |
| // | |||||
| // SA_DiceExpressionStringConstants.h | |||||
| // RPGBot | |||||
| // | |||||
| // Created by Sandy Achmiz on 1/2/16. | |||||
| // | |||||
| // | |||||
| #import <Foundation/Foundation.h> | |||||
| /***************************************************/ | |||||
| #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; |
| // | |||||
| // 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"; |
| // | |||||
| // SA_DiceFormatter.h | |||||
| // RPGBot | |||||
| // | |||||
| // Created by Sandy Achmiz on 12/31/15. | |||||
| // | |||||
| // | |||||
| #import <Foundation/Foundation.h> | |||||
| /*********************/ | |||||
| #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 |
| // | |||||
| // 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 |
| // | |||||
| // SA_DiceParser.h | |||||
| // RPGBot | |||||
| // | |||||
| // Created by Sandy Achmiz on 12/30/15. | |||||
| // | |||||
| // | |||||
| #import <Foundation/Foundation.h> | |||||
| /*********************/ | |||||
| #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 |
| // | |||||
| // 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 |