Ver código fonte

Initial commit.

master
achmizs 10 anos atrás
commit
5fcd8bdbd3

+ 38
- 0
SA_DB_ErrorDescriptions.plist Ver arquivo

@@ -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>

+ 36
- 0
SA_DB_StringFormatRules.plist Ver arquivo

@@ -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>

+ 26
- 0
SA_DiceBag.h Ver arquivo

@@ -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

+ 54
- 0
SA_DiceBag.m Ver arquivo

@@ -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

+ 183
- 0
SA_DiceBot.h Ver arquivo

@@ -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

+ 144
- 0
SA_DiceBot.m Ver arquivo

@@ -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

+ 18
- 0
SA_DiceErrorHandling.h Ver arquivo

@@ -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 */

+ 56
- 0
SA_DiceErrorHandling.m Ver arquivo

@@ -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;
}

+ 34
- 0
SA_DiceEvaluator.h Ver arquivo

@@ -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

+ 371
- 0
SA_DiceEvaluator.m Ver arquivo

@@ -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

+ 167
- 0
SA_DiceExpressionStringConstants.h Ver arquivo

@@ -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;

+ 84
- 0
SA_DiceExpressionStringConstants.m Ver arquivo

@@ -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";

+ 156
- 0
SA_DiceFormatter.h Ver arquivo

@@ -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

+ 447
- 0
SA_DiceFormatter.m Ver arquivo

@@ -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

+ 155
- 0
SA_DiceParser.h Ver arquivo

@@ -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

+ 413
- 0
SA_DiceParser.m Ver arquivo

@@ -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

Carregando…
Cancelar
Salvar