4 // Copyright 2016-2021 Said Achmiz.
5 // See LICENSE and README.md for more info.
7 #import "SA_DiceFormatter.h"
9 #import "SA_DiceExpressionStringConstants.h"
11 #import "SA_Utility.h"
13 /********************************/
14 #pragma mark File-scope variables
15 /********************************/
17 static SA_DiceFormatterBehavior _defaultFormatterBehavior = SA_DiceFormatterBehaviorLegacy;
18 static NSDictionary *_errorDescriptions;
19 static NSDictionary *_stringFormatRules;
21 /***************************************************/
22 #pragma mark - SA_DiceFormatter class implementation
23 /***************************************************/
25 @implementation SA_DiceFormatter {
26 SA_DiceFormatterBehavior _formatterBehavior;
29 /**********************************/
30 #pragma mark - Properties (general)
31 /**********************************/
33 -(void) setFormatterBehavior:(SA_DiceFormatterBehavior)newFormatterBehavior {
34 _formatterBehavior = newFormatterBehavior;
36 switch (_formatterBehavior) {
37 case SA_DiceFormatterBehaviorLegacy:
38 self.legacyModeErrorReportingEnabled = YES;
41 case SA_DiceFormatterBehaviorSimple:
42 case SA_DiceFormatterBehaviorModern:
43 case SA_DiceFormatterBehaviorFeepbot:
46 case SA_DiceFormatterBehaviorDefault:
48 self.formatterBehavior = SA_DiceFormatter.defaultFormatterBehavior;
53 -(SA_DiceFormatterBehavior) formatterBehavior {
54 return _formatterBehavior;
57 /******************************/
58 #pragma mark - Class properties
59 /******************************/
61 +(void) setDefaultFormatterBehavior:(SA_DiceFormatterBehavior)newDefaultFormatterBehavior {
62 if (newDefaultFormatterBehavior == SA_DiceFormatterBehaviorDefault) {
63 _defaultFormatterBehavior = SA_DiceFormatterBehaviorLegacy;
65 _defaultFormatterBehavior = newDefaultFormatterBehavior;
69 +(SA_DiceFormatterBehavior) defaultFormatterBehavior {
70 return _defaultFormatterBehavior;
73 +(NSDictionary *) stringFormatRules {
74 if (_stringFormatRules == nil) {
75 [SA_DiceFormatter loadStringFormatRules];
78 return _stringFormatRules;
81 /********************************************/
82 #pragma mark - Initializers & factory methods
83 /********************************************/
85 -(instancetype) init {
86 return [self initWithBehavior:SA_DiceFormatterBehaviorDefault];
89 -(instancetype) initWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior {
90 if (self = [super init]) {
91 self.formatterBehavior = formatterBehavior;
93 if (_errorDescriptions == nil) {
94 [SA_DiceFormatter loadErrorDescriptions];
97 if (_stringFormatRules == nil) {
98 [SA_DiceFormatter loadStringFormatRules];
104 +(instancetype) defaultFormatter {
105 return [[SA_DiceFormatter alloc] initWithBehavior:SA_DiceFormatterBehaviorDefault];
108 +(instancetype) formatterWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior {
109 return [[SA_DiceFormatter alloc] initWithBehavior:formatterBehavior];
112 /****************************/
113 #pragma mark - Public methods
114 /****************************/
116 -(NSString *) stringFromExpression:(SA_DiceExpression *)expression {
117 if (_formatterBehavior == SA_DiceFormatterBehaviorSimple) {
118 return [self simpleStringFromExpression:expression];
119 } else { // if(_formatterBehavior == SA_DiceFormatterBehaviorLegacy)
120 return [self legacyStringFromExpression:expression];
124 // NOT YET IMPLEMENTED
125 -(NSAttributedString *) attributedStringFromExpression:(SA_DiceExpression *)expression {
126 return [[NSAttributedString alloc] initWithString:[self stringFromExpression:expression]];
129 /**********************************************/
130 #pragma mark - “Legacy” behavior implementation
131 /**********************************************/
135 -(NSString *) legacyStringFromExpression:(SA_DiceExpression *)expression {
136 NSMutableString *formattedString = [NSMutableString string];
138 // Attach the formatted string representation of the expression itself.
139 [formattedString appendString:[self legacyStringFromIntermediaryExpression:expression]];
141 // An expression may contain either a result, or one or more errors.
142 // If a result is present, attach it. If errors are present, attach them
143 // only if error reporting is enabled.
144 if (expression.result != nil) {
145 [formattedString appendFormat:@" = %@", expression.result];
146 } else if ( _legacyModeErrorReportingEnabled == YES
147 && expression.errorBitMask != 0) {
148 [formattedString appendFormat:((__builtin_popcountl(expression.errorBitMask) == 1)
151 [SA_DiceFormatter descriptionForErrors:expression.errorBitMask]];
154 // Make all instances of the minus sign be represented with the proper,
155 // canonical minus sign.
156 return [SA_DiceFormatter rectifyMinusSignInString:formattedString];
159 -(NSString *) legacyStringFromIntermediaryExpression:(SA_DiceExpression *)expression {
161 In legacy behavior, we do not print the results of intermediate terms in
162 the expression tree (since the legacy output format was designed for
163 expressions generated by a parser that does not support parentheses,
164 doing so would not make sense anyway).
166 The exception is roll commands, where the result of a roll-and-sum command
167 is printed along with the rolls.
169 For this reasons, when we recursively retrieve the string representations
170 of sub-expressions, we call this method, not -[legacyStringFromExpression:].
173 switch (expression.type) {
174 case SA_DiceExpressionTerm_OPERATION: {
175 return [self legacyStringFromOperationExpression:expression];
178 case SA_DiceExpressionTerm_ROLL_COMMAND: {
179 return [self legacyStringFromRollCommandExpression:expression];
182 case SA_DiceExpressionTerm_ROLL_MODIFIER: {
183 return [self legacyStringFromRollModifierExpression:expression];
186 case SA_DiceExpressionTerm_VALUE: {
187 return [self legacyStringFromValueExpression:expression];
191 return expression.inputString;
197 -(NSString *) legacyStringFromOperationExpression:(SA_DiceExpression *)expression {
198 if (expression.operator == SA_DiceExpressionOperator_MINUS &&
199 expression.leftOperand == nil) {
200 // Check to see if the term is a negation operation.
201 return [@[ [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS],
202 [self legacyStringFromIntermediaryExpression:expression.rightOperand]
203 ] componentsJoinedByString:@""];
204 } else if (expression.operator == SA_DiceExpressionOperator_MINUS ||
205 expression.operator == SA_DiceExpressionOperator_PLUS ||
206 expression.operator == SA_DiceExpressionOperator_TIMES) {
207 // Check to see if the term is an addition, subtraction, or
208 // multiplication operation.
209 return [@[ [self legacyStringFromIntermediaryExpression:expression.leftOperand],
210 [SA_DiceFormatter canonicalRepresentationForOperator:expression.operator],
211 [self legacyStringFromIntermediaryExpression:expression.rightOperand]
212 ] componentsJoinedByString:@" "];
214 // If the operator is not one of the supported operators, default to
215 // outputting the input string.
216 return expression.inputString;
220 -(NSString *) legacyStringFromRollCommandExpression:(SA_DiceExpression *)expression {
222 In legacy behavior, we print the result of roll commands with the rolls
223 generated by the roll command. If a roll command generates a roll-related
224 error (any of the errors that begin with DIE_), we print “ERROR” in place
227 Legacy behavior assumes support for roll-and-sum only, so we do not need
228 to adjust the output format for different roll commands.
230 return [NSString stringWithFormat:@"%@%@%@ < %@%@ >",
231 [self legacyStringFromIntermediaryExpression:expression.dieCount],
232 [SA_DiceFormatter canonicalRepresentationForRollCommandDelimiter:expression.rollCommand],
233 [self legacyStringFromIntermediaryExpression:expression.dieSize],
234 ((expression.rolls != nil) ?
235 [NSString stringWithFormat:@"%@ = ",
236 [(expression.dieType == SA_DiceExpressionDice_FUDGE ?
237 [self formattedFudgeRolls:expression.rolls] :
239 ) componentsJoinedByString:@" "]] :
241 (expression.result ?: @"ERROR")];
244 -(NSArray *) formattedFudgeRolls:(NSArray <NSNumber *> *)rolls {
245 static NSDictionary *fudgeDieRollRepresentations;
246 static dispatch_once_t onceToken;
247 dispatch_once(&onceToken, ^{
248 fudgeDieRollRepresentations = @{ @(-1): [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS],
250 @(1): [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_PLUS]
254 return [rolls map:^NSString *(NSNumber *roll) {
255 return fudgeDieRollRepresentations[roll];
259 -(NSString *) legacyStringFromRollModifierExpression:(SA_DiceExpression *)expression {
261 In legacy behavior, we print the result of roll modifiers with the rolls
262 generated by the roll command, plus the modifications. If a roll modifier
263 generates an error, we print “ERROR” in place of any of the components.
265 Legacy behavior assumes support for the ‘keep’ modifier only, so we do not
266 need to adjust the output format for different roll modifiers.
268 NSUInteger keptHowMany = expression.rightOperand.result.unsignedIntegerValue;
269 return [NSString stringWithFormat:@"%@%@%@%@%@ < %@ less %@ leaves %@ = %@ >",
270 [self legacyStringFromIntermediaryExpression:expression.leftOperand.dieCount],
271 [SA_DiceFormatter canonicalRepresentationForRollCommandDelimiter:expression.leftOperand.rollCommand],
272 [self legacyStringFromIntermediaryExpression:expression.leftOperand.dieSize],
273 [SA_DiceFormatter canonicalRepresentationForRollModifierDelimiter:expression.rollModifier],
274 expression.rightOperand.result,
275 ((expression.leftOperand.rolls != nil) ?
276 [(expression.leftOperand.dieType == SA_DiceExpressionDice_FUDGE ?
277 [self formattedFudgeRolls:expression.rolls] :
278 expression.leftOperand.rolls
279 ) componentsJoinedByString:@" "] :
281 [(expression.leftOperand.dieType == SA_DiceExpressionDice_FUDGE ?
282 [self formattedFudgeRolls:[expression.rolls subarrayWithRange:NSRangeMake(keptHowMany, expression.rolls.count - keptHowMany)]] :
283 [expression.rolls subarrayWithRange:NSRangeMake(keptHowMany, expression.rolls.count - keptHowMany)]
284 ) componentsJoinedByString:@" "],
285 [(expression.leftOperand.dieType == SA_DiceExpressionDice_FUDGE ?
286 [self formattedFudgeRolls:[expression.rolls subarrayWithRange:NSRangeMake(0, keptHowMany)]] :
287 [expression.rolls subarrayWithRange:NSRangeMake(0, keptHowMany)]
288 ) componentsJoinedByString:@" "],
289 (expression.result ?: @"ERROR")];
292 -(NSString *) legacyStringFromValueExpression:(SA_DiceExpression *)expression {
293 if ([expression.inputString.lowercaseString isEqualToString:@"f"]) {
296 // We use the value for the ‘value’ property and not the ‘result’ property
297 // because they should be the same, and the ‘result’ property might not
298 // have a value (if the expression was not evaluated); this saves us
299 // having to compare it against nil, and saves code.
300 return [expression.value stringValue];
304 /**********************************************/
305 #pragma mark - “Simple” behavior implementation
306 /**********************************************/
308 -(NSString *) simpleStringFromExpression:(SA_DiceExpression *)expression {
309 NSString *formattedString = [NSString stringWithFormat:@"%@",
310 (expression.result ?: @"ERROR")];
312 // Make all instances of the minus sign be represented with the proper,
313 // canonical minus sign.
314 return [SA_DiceFormatter rectifyMinusSignInString:formattedString];
317 /****************************/
318 #pragma mark - Helper methods
319 /****************************/
321 +(NSString *) rectifyMinusSignInString:(NSString *)aString {
322 NSMutableString* sameStringButMutable = aString.mutableCopy;
324 NSString *validMinusSignCharacters = [SA_DiceFormatter stringFormatRules][SA_DB_VALID_CHARACTERS][SA_DB_VALID_OPERATOR_CHARACTERS][NSStringFromSA_DiceExpressionOperator(SA_DiceExpressionOperator_MINUS)];
325 [validMinusSignCharacters enumerateSubstringsInRange:NSRangeMake(0, validMinusSignCharacters.length)
326 options:NSStringEnumerationByComposedCharacterSequences
327 usingBlock:^(NSString *aValidMinusSignCharacter,
328 NSRange characterRange,
329 NSRange enclosingRange,
331 [sameStringButMutable replaceOccurrencesOfString:aValidMinusSignCharacter
332 withString:[SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS]
333 options:NSLiteralSearch
334 range:NSRangeMake(0, sameStringButMutable.length)];
337 return [sameStringButMutable copy];
340 +(void) loadErrorDescriptions {
341 NSString* errorDescriptionsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"SA_DB_ErrorDescriptions"
343 _errorDescriptions = [NSDictionary dictionaryWithContentsOfFile:errorDescriptionsPath];
344 if (!_errorDescriptions) {
345 NSLog(@"Could not load error descriptions!");
349 +(NSString *) descriptionForErrors:(NSUInteger)errorBitMask {
350 if (_errorDescriptions == nil) {
351 [SA_DiceFormatter loadErrorDescriptions];
354 NSMutableArray <NSString *> *errorDescriptions = [NSMutableArray array];
355 for (int i = 0; i <= 19; i++) {
356 if ((errorBitMask & (1 << i)) == 0)
358 NSString *errorName = NSStringFromSA_DiceExpressionError((SA_DiceExpressionError) (1 << i));
359 [errorDescriptions addObject:(_errorDescriptions[errorName] ?: errorName)];
362 return [errorDescriptions componentsJoinedByString:@" / "];
365 +(void) loadStringFormatRules {
366 NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME
368 _stringFormatRules = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath];
369 if (!_stringFormatRules) {
370 NSLog(@"Could not load string format rules!");
374 +(NSString *) canonicalRepresentationForOperator:(SA_DiceExpressionOperator)operator {
375 return [SA_DiceFormatter canonicalOperatorRepresentations][NSStringFromSA_DiceExpressionOperator(operator)];
378 +(NSDictionary *) canonicalOperatorRepresentations {
379 return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS];
382 +(NSString *) canonicalRepresentationForRollCommandDelimiter:(SA_DiceExpressionRollCommand)command {
383 return [SA_DiceFormatter canonicalRollCommandDelimiterRepresentations][NSStringFromSA_DiceExpressionRollCommand(command)];
386 +(NSDictionary *) canonicalRollCommandDelimiterRepresentations {
387 return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATIONS];
390 +(NSString *) canonicalRepresentationForRollModifierDelimiter:(SA_DiceExpressionRollModifier)modifier {
391 return [SA_DiceFormatter canonicalRollModifierDelimiterRepresentations][NSStringFromSA_DiceExpressionRollModifier(modifier)];
394 +(NSDictionary *) canonicalRollModifierDelimiterRepresentations {
395 return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_ROLL_MODIFIER_DELIMITER_REPRESENTATIONS];