Initial commit.
This commit is contained in:
commit
5fcd8bdbd3
38
SA_DB_ErrorDescriptions.plist
Normal file
38
SA_DB_ErrorDescriptions.plist
Normal file
@ -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
SA_DB_StringFormatRules.plist
Normal file
36
SA_DB_StringFormatRules.plist
Normal file
@ -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
SA_DiceBag.h
Normal file
26
SA_DiceBag.h
Normal file
@ -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
SA_DiceBag.m
Normal file
54
SA_DiceBag.m
Normal file
@ -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
SA_DiceBot.h
Normal file
183
SA_DiceBot.h
Normal file
@ -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
SA_DiceBot.m
Normal file
144
SA_DiceBot.m
Normal file
@ -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
SA_DiceErrorHandling.h
Normal file
18
SA_DiceErrorHandling.h
Normal file
@ -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
SA_DiceErrorHandling.m
Normal file
56
SA_DiceErrorHandling.m
Normal file
@ -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
SA_DiceEvaluator.h
Normal file
34
SA_DiceEvaluator.h
Normal file
@ -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
SA_DiceEvaluator.m
Normal file
371
SA_DiceEvaluator.m
Normal file
@ -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
SA_DiceExpressionStringConstants.h
Normal file
167
SA_DiceExpressionStringConstants.h
Normal file
@ -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
SA_DiceExpressionStringConstants.m
Normal file
84
SA_DiceExpressionStringConstants.m
Normal file
@ -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
SA_DiceFormatter.h
Normal file
156
SA_DiceFormatter.h
Normal file
@ -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
SA_DiceFormatter.m
Normal file
447
SA_DiceFormatter.m
Normal file
@ -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
SA_DiceParser.h
Normal file
155
SA_DiceParser.h
Normal file
@ -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
SA_DiceParser.m
Normal file
413
SA_DiceParser.m
Normal file
@ -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
|
||||
Loading…
Reference in New Issue
Block a user