| @@ -0,0 +1,38 @@ | |||
| <?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> | |||
| @@ -0,0 +1,36 @@ | |||
| <?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> | |||
| @@ -0,0 +1,26 @@ | |||
| // | |||
| // 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 | |||
| @@ -0,0 +1,54 @@ | |||
| // | |||
| // SA_DiceBag.m | |||
| // RPGBot | |||
| // | |||
| // Created by Sandy Achmiz on 12/30/15. | |||
| // | |||
| // | |||
| #import "SA_DiceBag.h" | |||
| /*******************************************/ | |||
| #pragma mark SA_DiceBag class implementation | |||
| /*******************************************/ | |||
| @implementation SA_DiceBag | |||
| /****************************/ | |||
| #pragma mark - Public methods | |||
| /****************************/ | |||
| - (unsigned long long)biggestPossibleDieSize | |||
| { | |||
| return UINT32_MAX; | |||
| } | |||
| - (unsigned long long)rollDie:(unsigned long long)dieSize | |||
| { | |||
| if(dieSize > UINT32_MAX) | |||
| { | |||
| return -1; | |||
| } | |||
| return (unsigned long long) arc4random_uniform((u_int32_t) dieSize) + 1; | |||
| } | |||
| - (NSArray *)rollNumber:(NSNumber *)number ofDice:(unsigned long long)dieSize | |||
| { | |||
| if(dieSize > UINT32_MAX) | |||
| { | |||
| return nil; | |||
| } | |||
| unsigned long long numRolls = number.unsignedLongLongValue; | |||
| NSMutableArray *rollsArray = [NSMutableArray arrayWithCapacity:numRolls]; | |||
| for(unsigned long long i = 0; i < numRolls; i++) | |||
| { | |||
| rollsArray[i] = @((unsigned long long) arc4random_uniform((u_int32_t) dieSize) + 1); | |||
| } | |||
| return rollsArray; | |||
| } | |||
| @end | |||
| @@ -0,0 +1,183 @@ | |||
| // | |||
| // SA_DiceBot.h | |||
| // RPGBot | |||
| // | |||
| // Created by Sandy Achmiz on 12/30/15. | |||
| // | |||
| // | |||
| /* | |||
| The SA_DiceBot class hierarchy works like this: | |||
| SA_DiceBot is the root class. A user of the SA_DiceBot package usually only | |||
| needs to create and manage objects of this class. An SA_DiceBot creates and | |||
| manages all other objects and classes in the package (directly or through its | |||
| other members), as needed. | |||
| An SA_DiceBot owns a single SA_DiceRoller and a single SA_DiceFormatter. | |||
| The objects owned by an SA_DiceBot are for the SA_DiceBot's internal use | |||
| only, and not accessible to users of the package. (But see SA_DiceRoller.h | |||
| and SA_DiceFormatter.h for information on standalone use.) | |||
| An SA_DiceRoller owns an SA_DiceParser and an SA_DiceEvaluator. An | |||
| SA_DiceEvaluator owns an SA_DiceBag. (See Figure 1, below.) | |||
| *--------------------------------------------------------------------------* | |||
| | Fig. 1. SA_DiceBot class hierarchy diagram. | | |||
| | | | |||
| | | | |||
| | SA_DiceBot | | |||
| | / \ | | |||
| | SA_DiceFormatter SA_DiceRoller | | |||
| | / \ | | |||
| | SA_DiceParser SA_DiceEvaluator | | |||
| | | | | |||
| | SA_DiceBag | | |||
| | | | |||
| *--------------------------------------------------------------------------* | |||
| When the SA_DiceBot receives a recognized command that involves rolling some | |||
| dice or otherwise evaluating some sort of string of semantically meaningful | |||
| text (like just adding some numbers), it extracts that string (which may | |||
| optionally include some text label) from the rest of the message body, and | |||
| passes it to the SA_DiceRoller. | |||
| The SA_DiceRoller strips off the label (if any), and passes the 'pure' string | |||
| to the SA_DiceParser. | |||
| The SA_DiceParser parses the string and produces an 'expression tree' | |||
| representation of the string. An expression tree is an NSDictionary with a | |||
| certain structure (which is described in SA_DiceExpressionStringConstants.h). | |||
| The SA_DiceRoller then passes the expression tree to the SA_DiceEvaluator. | |||
| The evaluator recursively traverses the expression tree in a depth-first | |||
| manner, computing the results of each subtree, and storing those results in | |||
| (a mutable copy of) the expression tree itself. Once the entire tree has been | |||
| evaluated, (a mutable copy of) it is returned to the SA_DiceRoller. The | |||
| SA_DiceRoller reattaches the stripped-off label (inserting it into the top | |||
| level of the expresion tree) and returns the evaluated expression tree | |||
| (now called a result) to the SA_DiceBot. | |||
| The SA_DiceBot passes the evaluated result tree to its SA_DiceFormatter. | |||
| The formatter traverses the tree, constructing a human-readable string form of | |||
| the results, with whatever formatting it (the formatter) has been configured | |||
| to provide. The formatter then returns this formatted result string to the | |||
| SA_DiceBot. | |||
| The SA_DiceBot then incorporates the formatted result string into some | |||
| appropriate reply message or messages, and sends the reply(ies) back to its | |||
| delegate, for transmission to the appropriate endpoint. | |||
| */ | |||
| #import <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 | |||
| @@ -0,0 +1,144 @@ | |||
| // | |||
| // SA_DiceBot.m | |||
| // RPGBot | |||
| // | |||
| // Created by Sandy Achmiz on 12/30/15. | |||
| // | |||
| // | |||
| #import "SA_DiceBot.h" | |||
| #import "SA_BotDelegate.h" | |||
| #import "SA_CommandResponder.h" | |||
| #import "SA_BotCommandResponder.h" | |||
| #import "SA_LegacyCommandResponder.h" | |||
| #import "SA_ErrorCatalog.h" | |||
| #import "NSString+SA_NSStringExtensions.h" | |||
| #import "NSRange-Conventional.h" | |||
| /*********************************************/ | |||
| #pragma mark - SA_DiceBot class implementation | |||
| /*********************************************/ | |||
| @implementation SA_DiceBot | |||
| /**************************/ | |||
| #pragma mark - Initializers | |||
| /**************************/ | |||
| - (instancetype)init | |||
| { | |||
| return [self initWithName:@"DIE_BOT"]; | |||
| } | |||
| - (instancetype)initWithName:(NSString *)name | |||
| { | |||
| if(self = [super initWithName:name]) | |||
| { | |||
| NSLog(NSLocalizedString(@"Initializing SA_DiceBot with name \"%@\"", @"{display name of the bot}"), name); | |||
| self.commandDesignatorCharacters = @".!/"; | |||
| [self loadDefaultCommandResponders]; | |||
| } | |||
| return self; | |||
| } | |||
| /****************************/ | |||
| #pragma mark - Public methods | |||
| /****************************/ | |||
| - (void)message:(NSString *)messageBody withInfo:(NSDictionary *)messageInfo | |||
| { | |||
| if([messageBody isEqualToString:@""]) | |||
| { | |||
| // Ignore empty messages. | |||
| } | |||
| else | |||
| { | |||
| NSRange commandRange; | |||
| BOOL byName; | |||
| // Is the message a possible command? That is, does it start with any of | |||
| // the permitted initial characters that designate a command? | |||
| NSString *firstChar = [messageBody substringToIndex:1]; | |||
| if([self.commandDesignatorCharacters containsCharactersInString:firstChar]) | |||
| { | |||
| commandRange = NSMakeRange(1, messageBody.length - 1); | |||
| byName = NO; | |||
| } | |||
| else | |||
| { | |||
| // We also recognize commands that come after mentions of the bot's | |||
| // name at the beginning of the message. | |||
| NSRange possibleNameRange = NSRangeMake(0, self.name.length); | |||
| if(messageBody.length > self.name.length && | |||
| [[messageBody substringWithRange:possibleNameRange] isEqualToString:self.name]) | |||
| { | |||
| commandRange = [messageBody rangeToEndFrom:[messageBody firstNonWhitespaceAfterRange:[messageBody firstWhitespaceAfterRange:possibleNameRange]]]; | |||
| byName = YES; | |||
| } | |||
| else | |||
| { | |||
| // Does not begin with a command. Ignore. | |||
| return; | |||
| } | |||
| } | |||
| // Extract the part of the string that is the actual command. | |||
| NSString *commandString = [messageBody substringWithRange:commandRange]; | |||
| // Get the replies for this command. | |||
| NSArray <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 | |||
| @@ -0,0 +1,18 @@ | |||
| // | |||
| // SA_DiceErrorHandling.h | |||
| // RPGBot | |||
| // | |||
| // Created by Sandy Achmiz on 1/11/16. | |||
| // | |||
| // | |||
| #ifndef SA_DiceErrorHandling_h | |||
| #define SA_DiceErrorHandling_h | |||
| #import <Foundation/Foundation.h> | |||
| void addErrorToExpression (NSString *error, NSMutableDictionary *expression); | |||
| void addErrorsFromExpressionToExpression (NSDictionary *sourceExpression, NSMutableDictionary *targetExpression); | |||
| NSArray <NSString *> *getErrorsForExpression (NSDictionary *expression); | |||
| #endif /* SA_DiceErrorHandling_h */ | |||
| @@ -0,0 +1,56 @@ | |||
| // | |||
| // SA_DiceErrorHandling.m | |||
| // RPGBot | |||
| // | |||
| // Created by Sandy Achmiz on 1/11/16. | |||
| // | |||
| // | |||
| #import "SA_DiceErrorHandling.h" | |||
| #import "SA_DiceExpressionStringConstants.h" | |||
| void addErrorToExpression (NSString *error, NSMutableDictionary *expression) | |||
| { | |||
| if(error == nil || expression == nil) | |||
| { | |||
| return; | |||
| } | |||
| if(expression[SA_DB_ERRORS] == nil) | |||
| { | |||
| expression[SA_DB_ERRORS] = [NSMutableArray <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; | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| // | |||
| // 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 | |||
| @@ -0,0 +1,371 @@ | |||
| // | |||
| // SA_DiceEvaluator.m | |||
| // RPGBot | |||
| // | |||
| // Created by Sandy Achmiz on 1/2/16. | |||
| // | |||
| // | |||
| #import "SA_DiceEvaluator.h" | |||
| #import "SA_DiceBag.h" | |||
| #import "SA_DiceParser.h" | |||
| #import "SA_DiceExpressionStringConstants.h" | |||
| #import "SA_DiceErrorHandling.h" | |||
| /**************************/ | |||
| #pragma mark Defined values | |||
| /**************************/ | |||
| #define DEFAULT_MAX_DIE_COUNT 1000 // One thousand | |||
| #define DEFAULT_MAX_DIE_SIZE 10000 // Ten thousand | |||
| /***************************************************/ | |||
| #pragma mark - SA_DiceEvaluator class implementation | |||
| /***************************************************/ | |||
| @implementation SA_DiceEvaluator | |||
| { | |||
| NSInteger _maxDieCount; | |||
| NSInteger _maxDieSize; | |||
| } | |||
| /************************/ | |||
| #pragma mark - Properties | |||
| /************************/ | |||
| - (NSInteger)maxDieCount | |||
| { | |||
| return _maxDieCount; | |||
| } | |||
| - (void)setMaxDieCount:(NSInteger)maxDieCount | |||
| { | |||
| if(maxDieCount < 1) | |||
| { | |||
| _maxDieCount = 1; | |||
| } | |||
| else if(maxDieCount > NSIntegerMax / _maxDieSize) | |||
| { | |||
| _maxDieCount = NSIntegerMax / _maxDieSize; | |||
| } | |||
| else | |||
| { | |||
| _maxDieCount = maxDieCount; | |||
| } | |||
| } | |||
| - (NSInteger)maxDieSize | |||
| { | |||
| return _maxDieSize; | |||
| } | |||
| - (void)setMaxDieSize:(NSInteger)maxDieSize | |||
| { | |||
| if(maxDieSize < 1) | |||
| { | |||
| _maxDieSize = 1; | |||
| } | |||
| else if(maxDieSize > [self.diceBag biggestPossibleDieSize] || maxDieSize > NSIntegerMax / _maxDieCount) | |||
| { | |||
| _maxDieSize = ([self.diceBag biggestPossibleDieSize] < (NSIntegerMax / _maxDieCount)) ? [_diceBag biggestPossibleDieSize] : NSIntegerMax / _maxDieCount; | |||
| } | |||
| else | |||
| { | |||
| _maxDieSize = maxDieSize; | |||
| } | |||
| } | |||
| /**************************/ | |||
| #pragma mark - Initializers | |||
| /**************************/ | |||
| - (instancetype)init | |||
| { | |||
| if(self = [super init]) | |||
| { | |||
| _maxDieCount = DEFAULT_MAX_DIE_COUNT; | |||
| _maxDieSize = DEFAULT_MAX_DIE_SIZE; | |||
| self.diceBag = [[SA_DiceBag alloc] init]; | |||
| } | |||
| return self; | |||
| } | |||
| /****************************/ | |||
| #pragma mark - Public methods | |||
| /****************************/ | |||
| - (NSMutableDictionary *)resultOfExpression:(NSDictionary *)expression | |||
| { | |||
| // Check to see if the expression is erroneous (i.e. the parser has judged | |||
| // that it is malformed, etc.). If so, decline to evaluate the expression; | |||
| // return (a mutable copy of) it, unchanged. | |||
| if(getErrorsForExpression(expression) != nil) | |||
| { | |||
| return [expression mutableCopy]; | |||
| } | |||
| /* | |||
| Even if an expression is not erroneous (i.e. if it has no syntax errors), | |||
| it may still not be possible to evaluate it. For example, '5d0' is a | |||
| perfectly well-formed die string, and will yield an expression tree as follows: | |||
| @{SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_ROLL_COMMAND, | |||
| SA_DB_ROLL_COMMAND : SA_DB_ROLL_COMMAND_SUM, | |||
| SA_DB_ROLL_DIE_COUNT : @{SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_VALUE, | |||
| SA_DB_VALUE : @(5) | |||
| }, | |||
| SA_DB_ROLL_DIE_SIZE : @{SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_VALUE, | |||
| SA_DB_VALUE : @(0) | |||
| } | |||
| } | |||
| This is, of course, an illegal expression; we can't roll a die of size 0 | |||
| (a die with zero sides?). | |||
| If we encounter such an illegal expression, we add an appropriate error to | |||
| the term. We are not required to set a value for the SA_DB_RESULT key in | |||
| such a case. | |||
| */ | |||
| // Check to see if the current term is an operation. | |||
| if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_OPERATION]) | |||
| { | |||
| return [self resultOfExpressionDescribingOperation:expression]; | |||
| } | |||
| // Check to see if the current term is a roll command. | |||
| else if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_ROLL_COMMAND]) | |||
| { | |||
| return [self resultOfExpressionDescribingRollCommand:expression]; | |||
| } | |||
| // If not an operation or a roll command, the current term can only be a | |||
| // simple value expression (term type of SA_DB_TERM_TYPE_VALUE). | |||
| else | |||
| { | |||
| return [self resultOfExpressionDescribingValue:expression]; | |||
| } | |||
| } | |||
| /****************************/ | |||
| #pragma mark - Helper methods | |||
| /****************************/ | |||
| - (NSMutableDictionary *)resultOfExpressionDescribingValue:(NSDictionary *)expression | |||
| { | |||
| NSMutableDictionary *result = [expression mutableCopy]; | |||
| result[SA_DB_RESULT] = result[SA_DB_VALUE]; | |||
| return result; | |||
| } | |||
| - (NSMutableDictionary *)resultOfExpressionDescribingRollCommand:(NSDictionary *)expression | |||
| { | |||
| NSMutableDictionary *result = [expression mutableCopy]; | |||
| // For now, only a simple sum is supported. Other sorts of roll commands | |||
| // may be added later. | |||
| if([result[SA_DB_ROLL_COMMAND] isEqualToString:SA_DB_ROLL_COMMAND_SUM]) | |||
| { | |||
| // First, recursively evaluate the expressions that represent the | |||
| // die size and die count. | |||
| result[SA_DB_ROLL_DIE_COUNT] = [self resultOfExpression:result[SA_DB_ROLL_DIE_COUNT]]; | |||
| result[SA_DB_ROLL_DIE_SIZE] = [self resultOfExpression:result[SA_DB_ROLL_DIE_SIZE]]; | |||
| // Evaluating those expressions may have generated an error(s); | |||
| // propagate any errors up to the current term. | |||
| addErrorsFromExpressionToExpression(result[SA_DB_ROLL_DIE_COUNT], result); | |||
| addErrorsFromExpressionToExpression(result[SA_DB_ROLL_DIE_SIZE], result); | |||
| // If indeed we've turned up errors, return. | |||
| if(getErrorsForExpression(result) != nil) | |||
| { | |||
| return result; | |||
| } | |||
| // Evaluating the two sub-expressions didn't generate errors; this means | |||
| // that we have successfully generated values for both the die count and | |||
| // the die size... | |||
| NSInteger dieCount = [result[SA_DB_ROLL_DIE_COUNT][SA_DB_RESULT] integerValue]; | |||
| NSInteger dieSize = [result[SA_DB_ROLL_DIE_SIZE][SA_DB_RESULT] integerValue]; | |||
| // ... but, the resulting values of the expressions may make it | |||
| // impossible to evaluate the roll command. Check to see whether the die | |||
| // count and die size have legal values. | |||
| if(dieCount < 0) | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_DIE_COUNT_NEGATIVE, result); | |||
| } | |||
| else if(dieCount > _maxDieCount) | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_DIE_COUNT_EXCESSIVE, result); | |||
| } | |||
| if(dieSize < 1) | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_DIE_SIZE_INVALID, result); | |||
| } | |||
| else if(dieSize > _maxDieSize) | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_DIE_SIZE_EXCESSIVE, result); | |||
| } | |||
| // If indeed the die count or die size fall outside of their allowed | |||
| // ranges, return. | |||
| if(getErrorsForExpression(result) != nil) | |||
| { | |||
| return result; | |||
| } | |||
| // The die count and die size have legal values. We can safely roll the | |||
| // requisite number of dice, and take the sum of the rolls (if needed). | |||
| // NOTE: _maxDieSize is gauranteed to be no greater than the largest die | |||
| // size that the SA_DiceBag can roll (this is enforced by the setter | |||
| // method for the maxDieSize property), so we need not check to see | |||
| // if the return value of rollDie: or rollNumber:ofDice: is valid. | |||
| // We are also gauranteed that the product of _maxDieCount and | |||
| // _maxDieSize is no greater than the largest unsigned value that can be | |||
| // stored by whatever numeric type we specify simple value terms (terms | |||
| // of type SA_DB_TERM_TYPE_VALUE) to contain (likewise enforced by the | |||
| // setters for both maxDieSize and maxDieCount), therefore we need not | |||
| // worry about overflow here. | |||
| if(dieCount == 0) | |||
| { | |||
| result[SA_DB_RESULT] = @(0); | |||
| result[SA_DB_ROLLS] = @[]; | |||
| } | |||
| else if(dieCount == 1) | |||
| { | |||
| NSNumber *roll = @([self.diceBag rollDie:dieSize]); | |||
| result[SA_DB_RESULT] = roll; | |||
| result[SA_DB_ROLLS] = @[roll]; | |||
| } | |||
| else | |||
| { | |||
| NSArray *rolls = [self.diceBag rollNumber:@(dieCount) ofDice:dieSize]; | |||
| result[SA_DB_RESULT] = [rolls valueForKeyPath:@"@sum.self"]; | |||
| result[SA_DB_ROLLS] = rolls; | |||
| } | |||
| // Return the successfully evaluated roll command expression. | |||
| return result; | |||
| } | |||
| else | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_UNKNOWN_ROLL_COMMAND, result); | |||
| return result; | |||
| } | |||
| } | |||
| - (NSMutableDictionary *)resultOfExpressionDescribingOperation:(NSDictionary *)expression | |||
| { | |||
| NSMutableDictionary *result = [expression mutableCopy]; | |||
| // First, recursively evaluate the expressions that represent the | |||
| // left-hand-side and right-hand-side operands. | |||
| result[SA_DB_OPERAND_LEFT] = [self resultOfExpression:result[SA_DB_OPERAND_LEFT]]; | |||
| result[SA_DB_OPERAND_RIGHT] = [self resultOfExpression:result[SA_DB_OPERAND_RIGHT]]; | |||
| // Evaluating the operand may have generated an error(s); propagate any | |||
| // errors up to the current term. | |||
| addErrorsFromExpressionToExpression(result[SA_DB_OPERAND_LEFT], result); | |||
| addErrorsFromExpressionToExpression(result[SA_DB_OPERAND_RIGHT], result); | |||
| // If indeed we've turned up errors, return. | |||
| if(getErrorsForExpression(result) != nil) | |||
| { | |||
| return result; | |||
| } | |||
| // Evaluating the operands didn't generate any errors. We have valid | |||
| // operands. | |||
| NSInteger leftOperand = [result[SA_DB_OPERAND_LEFT][SA_DB_RESULT] integerValue]; | |||
| NSInteger rightOperand = [result[SA_DB_OPERAND_RIGHT][SA_DB_RESULT] integerValue]; | |||
| // Check to see if the operation is subtraction. | |||
| if([result[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_MINUS]) | |||
| { | |||
| // First, we check for possible overflow... | |||
| if(leftOperand > 0 && rightOperand < 0 && NSIntegerMax + rightOperand < leftOperand) | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_INTEGER_OVERFLOW_SUBTRACTION, result); | |||
| return result; | |||
| } | |||
| else if(leftOperand < 0 && rightOperand > 0 && NSIntegerMin + rightOperand > leftOperand) | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_INTEGER_UNDERFLOW_SUBTRACTION, result); | |||
| return result; | |||
| } | |||
| // No overflow will occur. We can perform the subtraction operation. | |||
| result[SA_DB_RESULT] = @(leftOperand - rightOperand); | |||
| // Return the successfully evaluated negation expression. | |||
| return result; | |||
| } | |||
| // Check to see if the operation is addition. | |||
| else if([result[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_PLUS]) | |||
| { | |||
| // First, we check for possible overflow... | |||
| if(rightOperand > 0 && leftOperand > 0 && NSIntegerMax - rightOperand < leftOperand) | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_INTEGER_OVERFLOW_ADDITION, result); | |||
| return result; | |||
| } | |||
| else if(rightOperand < 0 && leftOperand < 0 && NSIntegerMin - rightOperand > leftOperand) | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_INTEGER_UNDERFLOW_ADDITION, result); | |||
| return result; | |||
| } | |||
| // No overflow will occur. We can perform the addition operation. | |||
| result[SA_DB_RESULT] = @(leftOperand + rightOperand); | |||
| // Return the successfully evaluated addition expression. | |||
| return result; | |||
| } | |||
| // Check to see if the operation is multiplication. | |||
| else if([result[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_TIMES]) | |||
| { | |||
| // First, we check for possible overflow... | |||
| if( ( leftOperand == NSIntegerMin && ( rightOperand != 0 || rightOperand != 1 ) ) || | |||
| ( rightOperand == NSIntegerMin && ( leftOperand != 0 || leftOperand != 1 ) ) || | |||
| ( leftOperand != 0 && ( (NSIntegerMax / ABS(leftOperand)) < rightOperand ) ) ) | |||
| { | |||
| if((leftOperand > 0 && rightOperand > 0) || (leftOperand < 0 && rightOperand < 0)) | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_INTEGER_OVERFLOW_MULTIPLICATION, result); | |||
| } | |||
| else | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION, result); | |||
| } | |||
| return result; | |||
| } | |||
| // No overflow will occur. We can perform the multiplication operation. | |||
| result[SA_DB_RESULT] = @(leftOperand * rightOperand); | |||
| // Return the successfully evaluated multiplication expression. | |||
| return result; | |||
| } | |||
| // The operation is not one of the supported operators. | |||
| else | |||
| { | |||
| // We add the appropriate error. We do not set a value for the | |||
| // SA_DB_RESULT key. | |||
| addErrorToExpression(SA_DB_ERROR_UNKNOWN_OPERATOR, result); | |||
| return result; | |||
| } | |||
| } | |||
| @end | |||
| @@ -0,0 +1,167 @@ | |||
| // | |||
| // 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; | |||
| @@ -0,0 +1,84 @@ | |||
| // | |||
| // SA_DiceExpressionStringConstants.m | |||
| // RPGBot | |||
| // | |||
| // Created by Sandy Achmiz on 1/2/16. | |||
| // | |||
| // | |||
| #import "SA_DiceExpressionStringConstants.h" | |||
| /***************************************************/ | |||
| #pragma mark String constants for expression parsing | |||
| /***************************************************/ | |||
| NSString * const SA_DB_TERM_TYPE = @"SA_DB_TERM_TYPE"; | |||
| NSString * const SA_DB_TERM_TYPE_VALUE = @"SA_DB_TERM_TYPE_VALUE"; | |||
| NSString * const SA_DB_TERM_TYPE_ROLL_COMMAND = @"SA_DB_TERM_TYPE_ROLL_COMMAND"; | |||
| NSString * const SA_DB_TERM_TYPE_OPERATION = @"SA_DB_TERM_TYPE_OPERATION"; | |||
| NSString * const SA_DB_TERM_TYPE_NONE = @"SA_DB_TERM_TYPE_NONE"; | |||
| NSString * const SA_DB_ERRORS = @"SA_DB_ERRORS"; | |||
| NSString * const SA_DB_ERROR_ROLL_STRING_EMPTY = @"SA_DB_ERROR_ROLL_STRING_EMPTY"; | |||
| NSString * const SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS = @"SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS"; | |||
| NSString * const SA_DB_OPERATOR = @"SA_DB_OPERATOR"; | |||
| NSString * const SA_DB_OPERATOR_MINUS = @"SA_DB_OPERATOR_MINUS"; | |||
| NSString * const SA_DB_OPERATOR_PLUS = @"SA_DB_OPERATOR_PLUS"; | |||
| NSString * const SA_DB_OPERATOR_TIMES = @"SA_DB_OPERATOR_TIMES"; | |||
| NSString * const SA_DB_OPERAND_LEFT = @"SA_DB_OPERAND_LEFT"; | |||
| NSString * const SA_DB_OPERAND_RIGHT = @"SA_DB_OPERAND_RIGHT"; | |||
| NSString * const SA_DB_ROLL_COMMAND = @"SA_DB_ROLL_COMMAND"; | |||
| NSString * const SA_DB_ROLL_COMMAND_SUM = @"SA_DB_ROLL_COMMAND_SUM"; | |||
| NSString * const SA_DB_ROLL_DIE_COUNT = @"SA_DB_ROLL_DIE_COUNT"; | |||
| NSString * const SA_DB_ROLL_DIE_SIZE = @"SA_DB_ROLL_DIE_SIZE"; | |||
| NSString * const SA_DB_VALUE = @"SA_DB_VALUE"; | |||
| NSString * const SA_DB_INPUT_STRING = @"SA_DB_INPUT_STRING"; | |||
| NSString * const SA_DB_ATTRIBUTED_INPUT_STRING = @"SA_DB_ATTRIBUTED_INPUT_STRING"; | |||
| /******************************************************/ | |||
| #pragma mark String constants for expression evaluation | |||
| /******************************************************/ | |||
| NSString * const SA_DB_ERROR_UNKNOWN_ROLL_COMMAND = @"SA_DB_ERROR_UNKNOWN_ROLL_COMMAND"; | |||
| NSString * const SA_DB_ERROR_DIE_COUNT_NEGATIVE = @"SA_DB_ERROR_DIE_COUNT_NEGATIVE"; | |||
| NSString * const SA_DB_ERROR_DIE_COUNT_EXCESSIVE = @"SA_DB_ERROR_DIE_COUNT_EXCESSIVE"; | |||
| NSString * const SA_DB_ERROR_DIE_SIZE_INVALID = @"SA_DB_ERROR_DIE_SIZE_INVALID"; | |||
| NSString * const SA_DB_ERROR_DIE_SIZE_EXCESSIVE = @"SA_DB_ERROR_DIE_SIZE_EXCESSIVE"; | |||
| NSString * const SA_DB_ERROR_UNKNOWN_OPERATOR = @"SA_DB_ERROR_UNKNOWN_OPERATOR"; | |||
| NSString * const SA_DB_ERROR_INVALID_EXPRESSION = @"SA_DB_ERROR_INVALID_EXPRESSION"; | |||
| NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_NEGATION = @"SA_DB_ERROR_INTEGER_OVERFLOW_NEGATION"; | |||
| NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_ADDITION = @"SA_DB_ERROR_INTEGER_OVERFLOW_ADDITION"; | |||
| NSString * const SA_DB_ERROR_INTEGER_UNDERFLOW_ADDITION = @"SA_DB_ERROR_INTEGER_UNDERFLOW_ADDITION"; | |||
| NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_SUBTRACTION = @"SA_DB_ERROR_INTEGER_OVERFLOW_SUBTRACTION"; | |||
| NSString * const SA_DB_ERROR_INTEGER_UNDERFLOW_SUBTRACTION = @"SA_DB_ERROR_INTEGER_UNDERFLOW_SUBTRACTION"; | |||
| NSString * const SA_DB_ERROR_INTEGER_OVERFLOW_MULTIPLICATION = @"SA_DB_ERROR_INTEGER_OVERFLOW_MULTIPLICATION"; | |||
| NSString * const SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION = @"SA_DB_ERROR_INTEGER_UNDERFLOW_MULTIPLICATION"; | |||
| NSString * const SA_DB_RESULT = @"SA_DB_RESULT"; | |||
| NSString * const SA_DB_ROLLS = @"SA_DB_ROLLS"; | |||
| /******************************************************/ | |||
| #pragma mark String constants for expression formatting | |||
| /******************************************************/ | |||
| NSString * const SA_DB_LABEL = @"SA_DB_LABEL"; | |||
| /***************************************************************/ | |||
| #pragma mark String constants for retrieving string format rules | |||
| /***************************************************************/ | |||
| NSString * const SA_DB_STRING_FORMAT_RULES_PLIST_NAME = @"SA_DB_StringFormatRules"; | |||
| NSString * const SA_DB_VALID_CHARACTERS = @"SA_DB_VALID_CHARACTERS"; | |||
| NSString * const SA_DB_VALID_NUMERAL_CHARACTERS = @"SA_DB_VALID_NUMERAL_CHARACTERS"; | |||
| NSString * const SA_DB_VALID_OPERATOR_CHARACTERS = @"SA_DB_VALID_OPERATOR_CHARACTERS"; | |||
| NSString * const SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS = @"SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS"; | |||
| NSString * const SA_DB_CANONICAL_REPRESENTATIONS = @"SA_DB_CANONICAL_REPRESENTATIONS"; | |||
| NSString * const SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS = @"SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS"; | |||
| NSString * const SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION = @"SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION"; | |||
| @@ -0,0 +1,156 @@ | |||
| // | |||
| // 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 | |||
| @@ -0,0 +1,447 @@ | |||
| // | |||
| // SA_DiceFormatter.m | |||
| // RPGBot | |||
| // | |||
| // Created by Sandy Achmiz on 12/31/15. | |||
| // | |||
| // | |||
| #import "SA_DiceFormatter.h" | |||
| #import "SA_DiceExpressionStringConstants.h" | |||
| #import "SA_DiceErrorHandling.h" | |||
| /********************************/ | |||
| #pragma mark File-scope variables | |||
| /********************************/ | |||
| static SA_DiceFormatterBehavior _defaultFormatterBehavior = SA_DiceFormatterBehaviorLegacy; | |||
| static NSDictionary *_errorDescriptions; | |||
| static NSDictionary *_stringFormatRules; | |||
| /**********************************************************/ | |||
| #pragma mark - SA_DiceFormatter class implementation | |||
| /**********************************************************/ | |||
| @implementation SA_DiceFormatter | |||
| { | |||
| SA_DiceFormatterBehavior _formatterBehavior; | |||
| } | |||
| /**********************************/ | |||
| #pragma mark - Properties (general) | |||
| /**********************************/ | |||
| - (void)setFormatterBehavior:(SA_DiceFormatterBehavior)newFormatterBehavior | |||
| { | |||
| _formatterBehavior = newFormatterBehavior; | |||
| switch (_formatterBehavior) | |||
| { | |||
| case SA_DiceFormatterBehaviorLegacy: | |||
| self.legacyModeErrorReportingEnabled = YES; | |||
| break; | |||
| case SA_DiceFormatterBehaviorSimple: | |||
| case SA_DiceFormatterBehaviorModern: | |||
| case SA_DiceFormatterBehaviorFeepbot: | |||
| break; | |||
| case SA_DiceFormatterBehaviorDefault: | |||
| default: | |||
| [self setFormatterBehavior:[SA_DiceFormatter defaultFormatterBehavior]]; | |||
| break; | |||
| } | |||
| } | |||
| - (SA_DiceFormatterBehavior)formatterBehavior | |||
| { | |||
| return _formatterBehavior; | |||
| } | |||
| /****************************************/ | |||
| #pragma mark - "Class property" accessors | |||
| /****************************************/ | |||
| + (void)setDefaultFormatterBehavior:(SA_DiceFormatterBehavior)newDefaultFormatterBehavior | |||
| { | |||
| if(newDefaultFormatterBehavior == SA_DiceFormatterBehaviorDefault) | |||
| { | |||
| _defaultFormatterBehavior = SA_DiceFormatterBehaviorLegacy; | |||
| } | |||
| else | |||
| { | |||
| _defaultFormatterBehavior = newDefaultFormatterBehavior; | |||
| } | |||
| } | |||
| + (SA_DiceFormatterBehavior)defaultFormatterBehavior | |||
| { | |||
| return _defaultFormatterBehavior; | |||
| } | |||
| + (NSDictionary *)stringFormatRules | |||
| { | |||
| if(_stringFormatRules == nil) | |||
| { | |||
| [SA_DiceFormatter loadStringFormatRules]; | |||
| } | |||
| return _stringFormatRules; | |||
| } | |||
| /********************************************/ | |||
| #pragma mark - Initializers & factory methods | |||
| /********************************************/ | |||
| - (instancetype)init | |||
| { | |||
| return [self initWithBehavior:SA_DiceFormatterBehaviorDefault]; | |||
| } | |||
| - (instancetype)initWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior | |||
| { | |||
| if(self = [super init]) | |||
| { | |||
| self.formatterBehavior = formatterBehavior; | |||
| if(_errorDescriptions == nil) | |||
| { | |||
| [SA_DiceFormatter loadErrorDescriptions]; | |||
| } | |||
| if(_stringFormatRules == nil) | |||
| { | |||
| [SA_DiceFormatter loadStringFormatRules]; | |||
| } | |||
| } | |||
| return self; | |||
| } | |||
| + (instancetype)defaultFormatter | |||
| { | |||
| return [[SA_DiceFormatter alloc] initWithBehavior:SA_DiceFormatterBehaviorDefault]; | |||
| } | |||
| + (instancetype)formatterWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior | |||
| { | |||
| return [[SA_DiceFormatter alloc] initWithBehavior:formatterBehavior]; | |||
| } | |||
| /****************************/ | |||
| #pragma mark - Public methods | |||
| /****************************/ | |||
| - (NSString *)stringFromExpression:(NSDictionary *)expression | |||
| { | |||
| if(_formatterBehavior == SA_DiceFormatterBehaviorSimple) | |||
| { | |||
| return [self simpleStringFromExpression:expression]; | |||
| } | |||
| else // if(_formatterBehavior == SA_DiceFormatterBehaviorLegacy) | |||
| { | |||
| return [self legacyStringFromExpression:expression]; | |||
| } | |||
| } | |||
| // NOT YET IMPLEMENTED | |||
| - (NSAttributedString *)attributedStringFromExpression:(NSDictionary *)expression | |||
| { | |||
| return [[NSAttributedString alloc] initWithString:[self stringFromExpression:expression]]; | |||
| } | |||
| /**********************************************/ | |||
| #pragma mark - "Legacy" behavior implementation | |||
| /**********************************************/ | |||
| // PROPERTIES | |||
| @synthesize legacyModeErrorReportingEnabled = _legacyModeErrorReportingEnabled; | |||
| // METHODS | |||
| - (NSString *)legacyStringFromExpression:(NSDictionary *)expression | |||
| { | |||
| __block NSMutableString *formattedString = [NSMutableString string]; | |||
| // Attach the formatted string representation of the expression itself. | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:expression]]; | |||
| // An expression may contain either a result, or one or more errors. | |||
| // If a result is present, attach it. If errors are present, attach them | |||
| // only if error reporting is enabled. | |||
| if(expression[SA_DB_RESULT] != nil) | |||
| { | |||
| [formattedString appendFormat:@" = %@", expression[SA_DB_RESULT]]; | |||
| } | |||
| else if(_legacyModeErrorReportingEnabled == YES && [getErrorsForExpression(expression) count] > 1) | |||
| { | |||
| if([getErrorsForExpression(expression) count] == 1) | |||
| { | |||
| [formattedString appendFormat:@" [ERROR: %@]", [SA_DiceFormatter descriptionForError:[getErrorsForExpression(expression) firstObject]]]; | |||
| } | |||
| else | |||
| { | |||
| [formattedString appendFormat:@" [ERRORS: "]; | |||
| [getErrorsForExpression(expression) enumerateObjectsUsingBlock:^(NSString *error, NSUInteger idx, BOOL *stop) | |||
| { | |||
| [formattedString appendString:[SA_DiceFormatter descriptionForError:error]]; | |||
| if(idx != [getErrorsForExpression(expression) count] - 1) | |||
| { | |||
| [formattedString appendFormat:@", "]; | |||
| } | |||
| else | |||
| { | |||
| [formattedString appendFormat:@"]"]; | |||
| } | |||
| }]; | |||
| } | |||
| } | |||
| return formattedString; | |||
| } | |||
| - (NSString *)legacyStringFromIntermediaryExpression:(NSDictionary *)expression | |||
| { | |||
| /* | |||
| In legacy behavior, we do not print the results of intermediate terms in | |||
| the expression tree (since the legacy output format was designed for | |||
| expressions generated by a parser that does not support parentheses, | |||
| doing so would not make sense anyway). | |||
| The exception is roll commands, where the result of a roll-and-sum command | |||
| is printed along with the rolls. | |||
| For this reasons, when we recursively retrieve the string representations | |||
| of sub-expressions, we call this method, not legacyStringFromExpression:. | |||
| */ | |||
| if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_OPERATION]) | |||
| { | |||
| return [self legacyStringFromOperationExpression:expression]; | |||
| } | |||
| else if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_ROLL_COMMAND]) | |||
| { | |||
| return [self legacyStringFromRollCommandExpression:expression]; | |||
| } | |||
| else if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_VALUE]) | |||
| { | |||
| return [self legacyStringFromValueExpression:expression]; | |||
| } | |||
| else // if([expression[SA_DB_TERM_TYPE] isEqualToString:SA_DB_TERM_TYPE_NONE]), probably | |||
| { | |||
| return expression[SA_DB_INPUT_STRING]; | |||
| } | |||
| } | |||
| - (NSString *)legacyStringFromOperationExpression:(NSDictionary *)expression | |||
| { | |||
| NSMutableString *formattedString = [NSMutableString string]; | |||
| // Check to see if the term is a negation or subtraction operation. | |||
| if([expression[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_MINUS]) | |||
| { | |||
| // Get the canonical representation for the operator. | |||
| NSString *operatorString = [SA_DiceFormatter canonicalRepresentationForOperator:SA_DB_OPERATOR_MINUS]; | |||
| // If we have a left operand, it's subtraction. If we do not, it's | |||
| // negation. | |||
| if(expression[SA_DB_OPERAND_LEFT] == nil) | |||
| { | |||
| // Get the operand. | |||
| NSDictionary *rightOperandExpression = expression[SA_DB_OPERAND_RIGHT]; | |||
| // Write out the string representations of operator and the | |||
| // right-hand-side expression. | |||
| [formattedString appendString:operatorString]; | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:rightOperandExpression]]; | |||
| return formattedString; | |||
| } | |||
| else | |||
| { | |||
| // Get the operands. | |||
| NSDictionary *leftOperandExpression = expression[SA_DB_OPERAND_LEFT]; | |||
| NSDictionary *rightOperandExpression = expression[SA_DB_OPERAND_RIGHT]; | |||
| // Write out the string representations of the left-hand-side | |||
| // expression, the operator, and the right-hand-side expression. | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:leftOperandExpression]]; | |||
| [formattedString appendFormat:@" %@ ", operatorString]; | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:rightOperandExpression]]; | |||
| return formattedString; | |||
| } | |||
| } | |||
| // Check to see if the term is an addition or subtraction operation. | |||
| else if([expression[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_PLUS]) | |||
| { | |||
| NSString *operatorString = [SA_DiceFormatter canonicalRepresentationForOperator:SA_DB_OPERATOR_PLUS]; | |||
| // Get the operands. | |||
| NSDictionary *leftOperandExpression = expression[SA_DB_OPERAND_LEFT]; | |||
| NSDictionary *rightOperandExpression = expression[SA_DB_OPERAND_RIGHT]; | |||
| // Write out the string representations of the left-hand-side | |||
| // expression, the operator, and the right-hand-side expression. | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:leftOperandExpression]]; | |||
| [formattedString appendFormat:@" %@ ", operatorString]; | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:rightOperandExpression]]; | |||
| return formattedString; | |||
| } | |||
| // Check to see if the term is a multiplication operation. | |||
| else if([expression[SA_DB_OPERATOR] isEqualToString:SA_DB_OPERATOR_TIMES]) | |||
| { | |||
| // Get the canonical representation for the operator. | |||
| NSString *operatorString = [SA_DiceFormatter canonicalRepresentationForOperator:SA_DB_OPERATOR_TIMES]; | |||
| // Get the operands. | |||
| NSDictionary *leftOperandExpression = expression[SA_DB_OPERAND_LEFT]; | |||
| NSDictionary *rightOperandExpression = expression[SA_DB_OPERAND_RIGHT]; | |||
| // Write out the string representations of the left-hand-side | |||
| // expression, the operator, and the right-hand-side expression. | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:leftOperandExpression]]; | |||
| [formattedString appendFormat:@" %@ ", operatorString]; | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:rightOperandExpression]]; | |||
| return formattedString; | |||
| } | |||
| else | |||
| { | |||
| // If the operator is not one of the supported operators, default to | |||
| // outputting the input string. | |||
| return expression[SA_DB_INPUT_STRING]; | |||
| } | |||
| } | |||
| - (NSString *)legacyStringFromRollCommandExpression:(NSDictionary *)expression | |||
| { | |||
| /* | |||
| In legacy behavior, we print the result of roll commands with the rolls | |||
| generated by the roll command. If a roll command generates a roll-related | |||
| error (any of the errors that begin with SA_DB_DIE_), we print "ERROR" | |||
| in place of a result. | |||
| Legacy behavior assumes support for roll-and-sum only, so we do not need | |||
| to adjust the output format for different roll commands. | |||
| */ | |||
| __block NSMutableString *formattedString = [NSMutableString string]; | |||
| // Append the die roll expression itself. | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:expression[SA_DB_ROLL_DIE_COUNT]]]; | |||
| [formattedString appendString:[SA_DiceFormatter canonicalRollCommandDelimiterRepresentation]]; | |||
| [formattedString appendString:[self legacyStringFromIntermediaryExpression:expression[SA_DB_ROLL_DIE_SIZE]]]; | |||
| [formattedString appendFormat:@" < "]; | |||
| // Append a list of the rolled values, if any. | |||
| if(expression[SA_DB_ROLLS] != nil && [expression[SA_DB_ROLLS] count] > 0) | |||
| { | |||
| [expression[SA_DB_ROLLS] enumerateObjectsUsingBlock:^(NSNumber *roll, NSUInteger idx, BOOL *stop) { | |||
| [formattedString appendFormat:@"%@ ", roll]; | |||
| }]; | |||
| [formattedString appendFormat:@"= "]; | |||
| } | |||
| // Append either the result, or the word 'ERROR'. | |||
| [formattedString appendFormat:@"%@ >", ((expression[SA_DB_RESULT] != nil) ? expression[SA_DB_RESULT] : @"ERROR")]; | |||
| return formattedString; | |||
| } | |||
| - (NSString *)legacyStringFromValueExpression:(NSDictionary *)expression | |||
| { | |||
| // We use the value for the SA_DB_VALUE key and not the SA_DB_RESULT key | |||
| // because they should be the same, and the SA_DB_RESULT key might not | |||
| // have a value (if the expression was not evaluated); this saves us | |||
| // having to compare it against nil, and saves code. | |||
| return [expression[SA_DB_VALUE] stringValue]; | |||
| } | |||
| /**********************************************/ | |||
| #pragma mark - "Simple" behavior implementation | |||
| /**********************************************/ | |||
| - (NSString *)simpleStringFromExpression:(NSDictionary *)expression | |||
| { | |||
| NSMutableString *formattedString = [NSMutableString string]; | |||
| if(expression[SA_DB_RESULT] != nil) | |||
| { | |||
| [formattedString appendFormat:@"%@", expression[SA_DB_RESULT]]; | |||
| } | |||
| else | |||
| { | |||
| [formattedString appendFormat:@"ERROR"]; | |||
| } | |||
| return formattedString; | |||
| } | |||
| /****************************/ | |||
| #pragma mark - Helper methods | |||
| /****************************/ | |||
| + (void)loadErrorDescriptions | |||
| { | |||
| NSString* errorDescriptionsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"SA_DB_ErrorDescriptions" ofType:@"plist"]; | |||
| _errorDescriptions = [NSDictionary dictionaryWithContentsOfFile:errorDescriptionsPath]; | |||
| if(_errorDescriptions) | |||
| { | |||
| NSLog(@"Error descriptions loaded successfully."); | |||
| } | |||
| else | |||
| { | |||
| NSLog(@"Could not load error descriptions!"); | |||
| } | |||
| } | |||
| + (NSString *)descriptionForError:(NSString *)error | |||
| { | |||
| if(_errorDescriptions == nil) | |||
| { | |||
| [SA_DiceFormatter loadErrorDescriptions]; | |||
| } | |||
| if(_errorDescriptions[error] != nil) | |||
| { | |||
| return _errorDescriptions[error]; | |||
| } | |||
| else | |||
| { | |||
| return error; | |||
| } | |||
| } | |||
| + (void)loadStringFormatRules | |||
| { | |||
| NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME ofType:@"plist"]; | |||
| _stringFormatRules = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath]; | |||
| if(_stringFormatRules) | |||
| { | |||
| NSLog(@"String format rules loaded successfully."); | |||
| } | |||
| else | |||
| { | |||
| NSLog(@"Could not load string format rules!"); | |||
| } | |||
| } | |||
| + (NSString *)canonicalRepresentationForOperator:(NSString *)operatorName | |||
| { | |||
| return [SA_DiceFormatter canonicalOperatorRepresentations][operatorName]; | |||
| } | |||
| + (NSDictionary *)canonicalOperatorRepresentations | |||
| { | |||
| return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS]; | |||
| } | |||
| + (NSString *)canonicalRollCommandDelimiterRepresentation | |||
| { | |||
| return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATION]; | |||
| } | |||
| @end | |||
| @@ -0,0 +1,155 @@ | |||
| // | |||
| // 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 | |||
| @@ -0,0 +1,413 @@ | |||
| // | |||
| // SA_DiceParser.m | |||
| // RPGBot | |||
| // | |||
| // Created by Sandy Achmiz on 12/30/15. | |||
| // | |||
| // | |||
| #import "SA_DiceParser.h" | |||
| #import "SA_DiceExpressionStringConstants.h" | |||
| #import "SA_DiceErrorHandling.h" | |||
| #import "NSString+SA_NSStringExtensions.h" | |||
| /********************************/ | |||
| #pragma mark File-scope variables | |||
| /********************************/ | |||
| static SA_DiceParserBehavior _defaultParserBehavior = SA_DiceParserBehaviorLegacy; | |||
| static NSDictionary *_validCharactersDict; | |||
| /************************************************/ | |||
| #pragma mark - SA_DiceParser class implementation | |||
| /************************************************/ | |||
| @implementation SA_DiceParser | |||
| { | |||
| SA_DiceParserBehavior _parserBehavior; | |||
| } | |||
| /************************/ | |||
| #pragma mark - Properties | |||
| /************************/ | |||
| - (void)setParserBehavior:(SA_DiceParserBehavior)newParserBehavior | |||
| { | |||
| _parserBehavior = newParserBehavior; | |||
| switch (_parserBehavior) | |||
| { | |||
| case SA_DiceParserBehaviorLegacy: | |||
| case SA_DiceParserBehaviorModern: | |||
| case SA_DiceParserBehaviorFeepbot: | |||
| break; | |||
| case SA_DiceParserBehaviorDefault: | |||
| default: | |||
| _parserBehavior = [SA_DiceParser defaultParserBehavior]; | |||
| break; | |||
| } | |||
| } | |||
| - (SA_DiceParserBehavior)parserBehavior | |||
| { | |||
| return _parserBehavior; | |||
| } | |||
| /****************************************/ | |||
| #pragma mark - "Class property" accessors | |||
| /****************************************/ | |||
| + (void)setDefaultParserBehavior:(SA_DiceParserBehavior)newDefaultParserBehavior | |||
| { | |||
| if(newDefaultParserBehavior == SA_DiceParserBehaviorDefault) | |||
| { | |||
| _defaultParserBehavior = SA_DiceParserBehaviorLegacy; | |||
| } | |||
| else | |||
| { | |||
| _defaultParserBehavior = newDefaultParserBehavior; | |||
| } | |||
| } | |||
| + (SA_DiceParserBehavior)defaultParserBehavior | |||
| { | |||
| return _defaultParserBehavior; | |||
| } | |||
| + (NSDictionary *)validCharactersDict | |||
| { | |||
| if(_validCharactersDict == nil) | |||
| { | |||
| [SA_DiceParser loadValidCharactersDict]; | |||
| } | |||
| return _validCharactersDict; | |||
| } | |||
| /********************************************/ | |||
| #pragma mark - Initializers & factory methods | |||
| /********************************************/ | |||
| - (instancetype)init | |||
| { | |||
| return [self initWithBehavior:SA_DiceParserBehaviorDefault]; | |||
| } | |||
| - (instancetype)initWithBehavior:(SA_DiceParserBehavior)parserBehavior | |||
| { | |||
| if(self = [super init]) | |||
| { | |||
| self.parserBehavior = parserBehavior; | |||
| if(_validCharactersDict == nil) | |||
| { | |||
| [SA_DiceParser loadValidCharactersDict]; | |||
| } | |||
| } | |||
| return self; | |||
| } | |||
| + (instancetype)defaultParser | |||
| { | |||
| return [[SA_DiceParser alloc] initWithBehavior:SA_DiceParserBehaviorDefault]; | |||
| } | |||
| + (instancetype)parserWithBehavior:(SA_DiceParserBehavior)parserBehavior | |||
| { | |||
| return [[SA_DiceParser alloc] initWithBehavior:parserBehavior]; | |||
| } | |||
| /****************************/ | |||
| #pragma mark - Public methods | |||
| /****************************/ | |||
| - (NSDictionary *)expressionForString:(NSString *)dieRollString | |||
| { | |||
| if(_parserBehavior == SA_DiceParserBehaviorLegacy) | |||
| { | |||
| return [self legacyExpressionForString:dieRollString]; | |||
| } | |||
| else | |||
| { | |||
| return @{}; | |||
| } | |||
| } | |||
| /**********************************************/ | |||
| #pragma mark - "Legacy" behavior implementation | |||
| /**********************************************/ | |||
| - (NSDictionary *)legacyExpressionForString:(NSString *)dieRollString | |||
| { | |||
| // Check for forbidden characters. | |||
| NSCharacterSet *forbiddenCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidCharacters]] invertedSet]; | |||
| if([dieRollString containsCharactersInSet:forbiddenCharacterSet]) | |||
| { | |||
| return @{ SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_NONE, | |||
| SA_DB_INPUT_STRING : dieRollString, | |||
| SA_DB_ERRORS : @[SA_DB_ERROR_ROLL_STRING_HAS_ILLEGAL_CHARACTERS] }; | |||
| } | |||
| // Since we have checked the entire string for forbidden characters, we can | |||
| // now begin parsing the string; there is no need to check substrings for | |||
| // illegal characters (which is why we do it only once, in this wrapper | |||
| // method). When constructing the expression tree, we call | |||
| // legacyExpressionForLegalString:, not legacyExpressionForString:, when | |||
| // recursively parsing substrings. | |||
| return [self legacyExpressionForLegalString:dieRollString]; | |||
| } | |||
| - (NSDictionary *)legacyExpressionForLegalString:(NSString *)dieRollString | |||
| { | |||
| // Make sure string is not empty. | |||
| if(dieRollString.length == 0) | |||
| { | |||
| return @{ SA_DB_TERM_TYPE : SA_DB_TERM_TYPE_NONE, | |||
| SA_DB_INPUT_STRING : dieRollString, | |||
| SA_DB_ERRORS : @[SA_DB_ERROR_ROLL_STRING_EMPTY] }; | |||
| } | |||
| // We now know the string describes one of the allowable expression types | |||
| // (probably; it could be malformed in some way other than being empty or | |||
| // containing forbidden characters, such as e.g. by starting with a + sign). | |||
| // Check to see if the top-level term is an operation. Note that we parse | |||
| // operator expressions left-associatively. | |||
| NSRange lastOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser allValidOperatorCharacters]] options:NSBackwardsSearch]; | |||
| if(lastOperatorRange.location != NSNotFound) | |||
| { | |||
| NSString *operator = [dieRollString substringWithRange:lastOperatorRange]; | |||
| return [self legacyExpressionForStringDescribingOperation:dieRollString withOperator:operator atRange:lastOperatorRange]; | |||
| } | |||
| // If not an operation, the top-level term might be a die roll command. | |||
| // Look for one of the characters recognized as valid die roll delimiters. | |||
| // Note that we parse roll commands left-associatively, therefore e.g. | |||
| // 5d6d10 parses as "roll N d10s, where N is the result of rolling 5d6". | |||
| NSRange lastRollCommandDelimiterRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:[SA_DiceParser validRollCommandDelimiterCharacters]] options:NSBackwardsSearch]; | |||
| if(lastRollCommandDelimiterRange.location != NSNotFound) | |||
| { | |||
| return [self legacyExpressionForStringDescribingRollCommand:dieRollString withDelimiterAtRange:lastRollCommandDelimiterRange]; | |||
| } | |||
| // If not an operation nor a roll command, the top-level term can only be | |||
| // a simple numeric value. | |||
| return [self legacyExpressionForStringDescribingNumericValue:dieRollString]; | |||
| } | |||
| - (NSDictionary *)legacyExpressionForStringDescribingOperation:(NSString *)dieRollString withOperator:(NSString *)operator atRange:(NSRange)operatorRange | |||
| { | |||
| NSMutableDictionary *expression; | |||
| // If the operator is at the beginning, this may be negation; we handle that | |||
| // case later below. Otherwise, it's a binary operator. | |||
| if(operatorRange.location != 0) | |||
| { | |||
| expression = [NSMutableDictionary dictionary]; | |||
| expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION; | |||
| expression[SA_DB_INPUT_STRING] = dieRollString; | |||
| // Operands of a binary operator are the expressions generated by | |||
| // parsing the strings before and after the addition operator. | |||
| expression[SA_DB_OPERAND_LEFT] = [self legacyExpressionForLegalString:[dieRollString substringToIndex:operatorRange.location]]; | |||
| expression[SA_DB_OPERAND_RIGHT] = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(operatorRange.location + operatorRange.length)]]; | |||
| // Check to see if the term is a subtraction operation. | |||
| if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS] containsCharactersInString:operator]) | |||
| { | |||
| expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_MINUS; | |||
| } | |||
| // Check to see if the term is an addition operation. | |||
| else if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_PLUS] containsCharactersInString:operator]) | |||
| { | |||
| expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_PLUS; | |||
| } | |||
| // Check to see if the term is a multiplication operation. | |||
| else if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_TIMES] containsCharactersInString:operator]) | |||
| { | |||
| // Look for other, lower-precedence operators to the left of the | |||
| // multiplication operator. If found, split the string there | |||
| // instead of at the current operator. | |||
| NSString *allLowerPrecedenceOperators = [NSString stringWithFormat:@"%@%@", [SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_PLUS], [SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS]]; | |||
| NSRange lastLowerPrecedenceOperatorRange = [dieRollString rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:allLowerPrecedenceOperators] options:NSBackwardsSearch range:NSMakeRange(1, operatorRange.location - 1)]; | |||
| if(lastLowerPrecedenceOperatorRange.location != NSNotFound) | |||
| { | |||
| NSString *lowerPrecedenceOperator = [dieRollString substringWithRange:lastLowerPrecedenceOperatorRange]; | |||
| return [self legacyExpressionForStringDescribingOperation:dieRollString withOperator:lowerPrecedenceOperator atRange:lastLowerPrecedenceOperatorRange]; | |||
| } | |||
| expression[SA_DB_OPERATOR] = SA_DB_OPERATOR_TIMES; | |||
| } | |||
| else | |||
| { | |||
| addErrorToExpression(SA_DB_ERROR_UNKNOWN_OPERATOR, expression); | |||
| } | |||
| } | |||
| // Check to see if the term is a negation operation (we can only reach this | |||
| // case if the subtraction operator is at the beginning). | |||
| else | |||
| { | |||
| if(dieRollString.length == operatorRange.length) | |||
| { | |||
| expression = [NSMutableDictionary dictionary]; | |||
| expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION; | |||
| expression[SA_DB_INPUT_STRING] = dieRollString; | |||
| addErrorToExpression(SA_DB_ERROR_INVALID_EXPRESSION, expression); | |||
| return expression; | |||
| } | |||
| if([[SA_DiceParser validCharactersForOperator:SA_DB_OPERATOR_MINUS] containsCharactersInString:operator]) | |||
| { | |||
| // It turns out this isn't actually an operation expression. It's a | |||
| // simple value expression with a negative number. | |||
| // This next line is a hack to account for the fact that Cocoa's | |||
| // Unicode compliance is incomplete. :( NSString's integerValue | |||
| // method only accepts the hyphen as a negation sign when reading a | |||
| // number - not any of the Unicode characters which officially | |||
| // symbolize negation! But we are more modern-minded, and accept | |||
| // arbitrary symbols as minus-sign. For proper parsing, though, | |||
| // we have to replace it like this... | |||
| NSString *fixedRollString = [dieRollString stringByReplacingCharactersInRange:operatorRange withString:@"-"]; | |||
| return [self legacyExpressionForStringDescribingNumericValue:fixedRollString]; | |||
| } | |||
| else | |||
| { | |||
| expression = [NSMutableDictionary dictionary]; | |||
| expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_OPERATION; | |||
| expression[SA_DB_INPUT_STRING] = dieRollString; | |||
| addErrorToExpression(SA_DB_ERROR_INVALID_EXPRESSION, expression); | |||
| return expression; | |||
| } | |||
| } | |||
| // The operands have now been parsed recursively; this parsing may have | |||
| // generated one or more errors. Inherit any error(s) from the | |||
| // error-generating operand(s). | |||
| addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_RIGHT], expression); | |||
| addErrorsFromExpressionToExpression(expression[SA_DB_OPERAND_LEFT], expression); | |||
| return expression; | |||
| } | |||
| - (NSDictionary *)legacyExpressionForStringDescribingRollCommand:(NSString *)dieRollString withDelimiterAtRange:(NSRange)delimiterRange | |||
| { | |||
| NSMutableDictionary *expression = [NSMutableDictionary dictionary]; | |||
| expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_ROLL_COMMAND; | |||
| expression[SA_DB_INPUT_STRING] = dieRollString; | |||
| // For now, only one kind of roll command is supported - roll-and-sum. | |||
| // This rolls one or more dice of a given sort, and determines the sum of | |||
| // their rolled values. | |||
| // In the future, support for other, more complex roll commands might be | |||
| // added, such as "roll several and return the highest", exploding dice, | |||
| // etc. | |||
| expression[SA_DB_ROLL_COMMAND] = SA_DB_ROLL_COMMAND_SUM; | |||
| // Check to see if the delimiter is the initial character of the roll | |||
| // string. If so (i.e. if the die count is omitted), we assume it to be 1 | |||
| // (i.e. 'd6' is read as '1d6'). | |||
| if(delimiterRange.location == 0) | |||
| { | |||
| expression[SA_DB_ROLL_DIE_COUNT] = [self legacyExpressionForStringDescribingNumericValue:@"1"]; | |||
| } | |||
| else | |||
| { | |||
| // The die count is the expression generated by parsing the string | |||
| // before the delimiter. | |||
| expression[SA_DB_ROLL_DIE_COUNT] = [self legacyExpressionForLegalString:[dieRollString substringToIndex:delimiterRange.location]]; | |||
| } | |||
| // The die size is the expression generated by parsing the string after the | |||
| // delimiter. | |||
| expression[SA_DB_ROLL_DIE_SIZE] = [self legacyExpressionForLegalString:[dieRollString substringFromIndex:(delimiterRange.location + delimiterRange.length)]]; | |||
| // The die count and die size have now been parsed recursively; this parsing | |||
| // may have generated one or more errors. Inherit any error(s) from the | |||
| // error-generating sub-terms. | |||
| addErrorsFromExpressionToExpression(expression[SA_DB_ROLL_DIE_COUNT], expression); | |||
| addErrorsFromExpressionToExpression(expression[SA_DB_ROLL_DIE_SIZE], expression); | |||
| return expression; | |||
| } | |||
| - (NSDictionary *)legacyExpressionForStringDescribingNumericValue:(NSString *)dieRollString | |||
| { | |||
| NSMutableDictionary *expression = [NSMutableDictionary dictionary]; | |||
| expression[SA_DB_TERM_TYPE] = SA_DB_TERM_TYPE_VALUE; | |||
| expression[SA_DB_INPUT_STRING] = dieRollString; | |||
| expression[SA_DB_VALUE] = @(dieRollString.integerValue); | |||
| return expression; | |||
| } | |||
| /****************************/ | |||
| #pragma mark - Helper methods | |||
| /****************************/ | |||
| + (void)loadValidCharactersDict | |||
| { | |||
| NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME ofType:@"plist"]; | |||
| _validCharactersDict = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath][SA_DB_VALID_CHARACTERS]; | |||
| if(_validCharactersDict) | |||
| { | |||
| NSLog(@"Valid characters dictionary loaded successfully."); | |||
| } | |||
| else | |||
| { | |||
| NSLog(@"Could not load valid characters dictionary!"); | |||
| } | |||
| } | |||
| + (NSString *)allValidCharacters | |||
| { | |||
| NSMutableString *validCharactersString = [NSMutableString string]; | |||
| [validCharactersString appendString:[SA_DiceParser validNumeralCharacters]]; | |||
| [validCharactersString appendString:[SA_DiceParser validRollCommandDelimiterCharacters]]; | |||
| [validCharactersString appendString:[SA_DiceParser allValidOperatorCharacters]]; | |||
| return validCharactersString; | |||
| } | |||
| + (NSString *)allValidOperatorCharacters | |||
| { | |||
| NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict]; | |||
| __block NSMutableString *validOperatorCharactersString = [NSMutableString string]; | |||
| [validCharactersDict[SA_DB_VALID_OPERATOR_CHARACTERS] enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { | |||
| [validOperatorCharactersString appendString:value]; | |||
| }]; | |||
| return validOperatorCharactersString; | |||
| } | |||
| + (NSString *)validCharactersForOperator:(NSString *)operatorName | |||
| { | |||
| return [SA_DiceParser validCharactersDict][SA_DB_VALID_OPERATOR_CHARACTERS][operatorName]; | |||
| } | |||
| + (NSString *)validNumeralCharacters | |||
| { | |||
| NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict]; | |||
| return validCharactersDict[SA_DB_VALID_NUMERAL_CHARACTERS]; | |||
| } | |||
| + (NSString *)validRollCommandDelimiterCharacters | |||
| { | |||
| NSDictionary *validCharactersDict = [SA_DiceParser validCharactersDict]; | |||
| return validCharactersDict[SA_DB_VALID_ROLL_COMMAND_DELIMITER_CHARACTERS]; | |||
| } | |||
| @end | |||