Updated LICENSE and info comments
[SA_Dice.git] / SA_DiceFormatter.m
blob2beba6a6d91b23316ab7f61964890b5a349c94f6
1 //
2 //  SA_DiceFormatter.m
3 //
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;
35         
36         switch (_formatterBehavior) {
37                 case SA_DiceFormatterBehaviorLegacy:
38                         self.legacyModeErrorReportingEnabled = YES;
39                         break;
41                 case SA_DiceFormatterBehaviorSimple:
42                 case SA_DiceFormatterBehaviorModern:
43                 case SA_DiceFormatterBehaviorFeepbot:
44                         break;
45                         
46                 case SA_DiceFormatterBehaviorDefault:
47                 default:
48                         self.formatterBehavior = SA_DiceFormatter.defaultFormatterBehavior;
49                         break;
50         }
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;
64         } else {
65                 _defaultFormatterBehavior = newDefaultFormatterBehavior;
66         }
69 +(SA_DiceFormatterBehavior) defaultFormatterBehavior {
70         return _defaultFormatterBehavior;
73 +(NSDictionary *) stringFormatRules {
74         if (_stringFormatRules == nil) {
75                 [SA_DiceFormatter loadStringFormatRules];
76         }
77         
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;
92                 
93                 if (_errorDescriptions == nil) {
94                         [SA_DiceFormatter loadErrorDescriptions];
95                 }
97                 if (_stringFormatRules == nil) {
98                         [SA_DiceFormatter loadStringFormatRules];
99                 }
100         }
101         return self;
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];
121         }
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 /**********************************************/
133 // METHODS
135 -(NSString *) legacyStringFromExpression:(SA_DiceExpression *)expression {
136         NSMutableString *formattedString = [NSMutableString string];
137         
138         // Attach the formatted string representation of the expression itself.
139         [formattedString appendString:[self legacyStringFromIntermediaryExpression:expression]];
140         
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)
149                                                                            ? @" [ERROR: %@]"
150                                                                            : @" [ERRORS: %@]"),
151                  [SA_DiceFormatter descriptionForErrors:expression.errorBitMask]];
152         }
153         
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 {
160         /*
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).
165          
166          The exception is roll commands, where the result of a roll-and-sum command
167          is printed along with the rolls.
168          
169          For this reasons, when we recursively retrieve the string representations
170          of sub-expressions, we call this method, not -[legacyStringFromExpression:].
171          */
173         switch (expression.type) {
174                 case SA_DiceExpressionTerm_OPERATION: {
175                         return [self legacyStringFromOperationExpression:expression];
176                         break;
177                 }
178                 case SA_DiceExpressionTerm_ROLL_COMMAND: {
179                         return [self legacyStringFromRollCommandExpression:expression];
180                         break;
181                 }
182                 case SA_DiceExpressionTerm_ROLL_MODIFIER: {
183                         return [self legacyStringFromRollModifierExpression:expression];
184                         break;
185                 }
186                 case SA_DiceExpressionTerm_VALUE: {
187                         return [self legacyStringFromValueExpression:expression];
188                         break;
189                 }
190                 default: {
191                         return expression.inputString;
192                         break;
193                 }
194         }
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:@" "];
213         } else {
214                 // If the operator is not one of the supported operators, default to
215                 // outputting the input string.
216                 return expression.inputString;
217         }
220 -(NSString *) legacyStringFromRollCommandExpression:(SA_DiceExpression *)expression {
221         /*
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 
225          of a result.
226          
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.
229         */
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] :
238                                 expression.rolls
239                                 ) componentsJoinedByString:@" "]] :
240                          @""),
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],
249                                                                                  @(0): @"0",
250                                                                                  @(1): [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_PLUS]
251                                                                                  };
252         });
254         return [rolls map:^NSString *(NSNumber *roll) {
255                 return fudgeDieRollRepresentations[roll];
256         }];
259 -(NSString *) legacyStringFromRollModifierExpression:(SA_DiceExpression *)expression {
260         /*
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.
267          */
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:@" "] :
280                          @""),
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"]) {
294                 return @"F";
295         } else {
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];
301         }
304 /**********************************************/
305 #pragma mark - “Simple” behavior implementation
306 /**********************************************/
308 -(NSString *) simpleStringFromExpression:(SA_DiceExpression *)expression {
309         NSString *formattedString = [NSString stringWithFormat:@"%@", 
310                                                                  (expression.result ?: @"ERROR")];
311         
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;
323         
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,
330                                                                                                                    BOOL *stop) {
331                  [sameStringButMutable replaceOccurrencesOfString:aValidMinusSignCharacter 
332                                                                                            withString:[SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS]
333                                                                                                   options:NSLiteralSearch 
334                                                                                                         range:NSRangeMake(0, sameStringButMutable.length)];
335          }];
336         
337         return [sameStringButMutable copy];
340 +(void) loadErrorDescriptions {
341         NSString* errorDescriptionsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"SA_DB_ErrorDescriptions"
342                                                                                                                                                                            ofType:@"plist"];
343         _errorDescriptions = [NSDictionary dictionaryWithContentsOfFile:errorDescriptionsPath];
344         if (!_errorDescriptions) {
345                 NSLog(@"Could not load error descriptions!");
346         }
349 +(NSString *) descriptionForErrors:(NSUInteger)errorBitMask {
350         if (_errorDescriptions == nil) {
351                 [SA_DiceFormatter loadErrorDescriptions];
352         }
354         NSMutableArray <NSString *> *errorDescriptions = [NSMutableArray array];
355         for (int i = 0; i <= 19; i++) {
356                 if ((errorBitMask & (1 << i)) == 0)
357                         continue;
358                 NSString *errorName = NSStringFromSA_DiceExpressionError((SA_DiceExpressionError) (1 << i));
359                 [errorDescriptions addObject:(_errorDescriptions[errorName] ?: errorName)];
360         }
362         return [errorDescriptions componentsJoinedByString:@" / "];
365 +(void) loadStringFormatRules {
366         NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME
367                                                                                                                                                                            ofType:@"plist"];
368         _stringFormatRules = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath];
369         if (!_stringFormatRules) {
370                 NSLog(@"Could not load string format rules!");
371         }
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];
398 @end