A set of classes for parsing, evaluating, and formatting die roll strings.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

SA_DiceFormatter.m 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. //
  2. // SA_DiceFormatter.m
  3. //
  4. // Copyright (c) 2016 Said Achmiz.
  5. //
  6. // This software is licensed under the MIT license.
  7. // See the file "LICENSE" for more information.
  8. #import "SA_DiceFormatter.h"
  9. #import "SA_DiceExpressionStringConstants.h"
  10. #import "SA_Utility.h"
  11. /********************************/
  12. #pragma mark File-scope variables
  13. /********************************/
  14. static SA_DiceFormatterBehavior _defaultFormatterBehavior = SA_DiceFormatterBehaviorLegacy;
  15. static NSDictionary *_errorDescriptions;
  16. static NSDictionary *_stringFormatRules;
  17. /***************************************************/
  18. #pragma mark - SA_DiceFormatter class implementation
  19. /***************************************************/
  20. @implementation SA_DiceFormatter {
  21. SA_DiceFormatterBehavior _formatterBehavior;
  22. }
  23. /**********************************/
  24. #pragma mark - Properties (general)
  25. /**********************************/
  26. -(void) setFormatterBehavior:(SA_DiceFormatterBehavior)newFormatterBehavior {
  27. _formatterBehavior = newFormatterBehavior;
  28. switch (_formatterBehavior) {
  29. case SA_DiceFormatterBehaviorLegacy:
  30. self.legacyModeErrorReportingEnabled = YES;
  31. break;
  32. case SA_DiceFormatterBehaviorSimple:
  33. case SA_DiceFormatterBehaviorModern:
  34. case SA_DiceFormatterBehaviorFeepbot:
  35. break;
  36. case SA_DiceFormatterBehaviorDefault:
  37. default:
  38. self.formatterBehavior = SA_DiceFormatter.defaultFormatterBehavior;
  39. break;
  40. }
  41. }
  42. -(SA_DiceFormatterBehavior) formatterBehavior {
  43. return _formatterBehavior;
  44. }
  45. /******************************/
  46. #pragma mark - Class properties
  47. /******************************/
  48. +(void) setDefaultFormatterBehavior:(SA_DiceFormatterBehavior)newDefaultFormatterBehavior {
  49. if (newDefaultFormatterBehavior == SA_DiceFormatterBehaviorDefault) {
  50. _defaultFormatterBehavior = SA_DiceFormatterBehaviorLegacy;
  51. } else {
  52. _defaultFormatterBehavior = newDefaultFormatterBehavior;
  53. }
  54. }
  55. +(SA_DiceFormatterBehavior) defaultFormatterBehavior {
  56. return _defaultFormatterBehavior;
  57. }
  58. +(NSDictionary *) stringFormatRules {
  59. if (_stringFormatRules == nil) {
  60. [SA_DiceFormatter loadStringFormatRules];
  61. }
  62. return _stringFormatRules;
  63. }
  64. /********************************************/
  65. #pragma mark - Initializers & factory methods
  66. /********************************************/
  67. -(instancetype) init {
  68. return [self initWithBehavior:SA_DiceFormatterBehaviorDefault];
  69. }
  70. -(instancetype) initWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior {
  71. if (self = [super init]) {
  72. self.formatterBehavior = formatterBehavior;
  73. if (_errorDescriptions == nil) {
  74. [SA_DiceFormatter loadErrorDescriptions];
  75. }
  76. if (_stringFormatRules == nil) {
  77. [SA_DiceFormatter loadStringFormatRules];
  78. }
  79. }
  80. return self;
  81. }
  82. +(instancetype) defaultFormatter {
  83. return [[SA_DiceFormatter alloc] initWithBehavior:SA_DiceFormatterBehaviorDefault];
  84. }
  85. +(instancetype) formatterWithBehavior:(SA_DiceFormatterBehavior)formatterBehavior {
  86. return [[SA_DiceFormatter alloc] initWithBehavior:formatterBehavior];
  87. }
  88. /****************************/
  89. #pragma mark - Public methods
  90. /****************************/
  91. -(NSString *) stringFromExpression:(SA_DiceExpression *)expression {
  92. if (_formatterBehavior == SA_DiceFormatterBehaviorSimple) {
  93. return [self simpleStringFromExpression:expression];
  94. } else { // if(_formatterBehavior == SA_DiceFormatterBehaviorLegacy)
  95. return [self legacyStringFromExpression:expression];
  96. }
  97. }
  98. // NOT YET IMPLEMENTED
  99. -(NSAttributedString *) attributedStringFromExpression:(SA_DiceExpression *)expression {
  100. return [[NSAttributedString alloc] initWithString:[self stringFromExpression:expression]];
  101. }
  102. /**********************************************/
  103. #pragma mark - “Legacy” behavior implementation
  104. /**********************************************/
  105. // METHODS
  106. -(NSString *) legacyStringFromExpression:(SA_DiceExpression *)expression {
  107. NSMutableString *formattedString = [NSMutableString string];
  108. // Attach the formatted string representation of the expression itself.
  109. [formattedString appendString:[self legacyStringFromIntermediaryExpression:expression]];
  110. // An expression may contain either a result, or one or more errors.
  111. // If a result is present, attach it. If errors are present, attach them
  112. // only if error reporting is enabled.
  113. if (expression.result != nil) {
  114. [formattedString appendFormat:@" = %@", expression.result];
  115. } else if (_legacyModeErrorReportingEnabled == YES &&
  116. expression.errorBitMask != 0) {
  117. [formattedString appendFormat:((__builtin_popcountl(expression.errorBitMask) == 1) ? @" [ERROR: %@]" : @" [ERRORS: %@]"),
  118. [SA_DiceFormatter descriptionForErrors:expression.errorBitMask]];
  119. }
  120. // Make all instances of the minus sign be represented with the proper,
  121. // canonical minus sign.
  122. return [SA_DiceFormatter rectifyMinusSignInString:formattedString];
  123. }
  124. -(NSString *) legacyStringFromIntermediaryExpression:(SA_DiceExpression *)expression {
  125. /*
  126. In legacy behavior, we do not print the results of intermediate terms in
  127. the expression tree (since the legacy output format was designed for
  128. expressions generated by a parser that does not support parentheses,
  129. doing so would not make sense anyway).
  130. The exception is roll commands, where the result of a roll-and-sum command
  131. is printed along with the rolls.
  132. For this reasons, when we recursively retrieve the string representations
  133. of sub-expressions, we call this method, not legacyStringFromExpression:.
  134. */
  135. switch (expression.type) {
  136. case SA_DiceExpressionTerm_OPERATION: {
  137. return [self legacyStringFromOperationExpression:expression];
  138. break;
  139. }
  140. case SA_DiceExpressionTerm_ROLL_COMMAND: {
  141. return [self legacyStringFromRollCommandExpression:expression];
  142. break;
  143. }
  144. case SA_DiceExpressionTerm_ROLL_MODIFIER: {
  145. return [self legacyStringFromRollModifierExpression:expression];
  146. break;
  147. }
  148. case SA_DiceExpressionTerm_VALUE: {
  149. return [self legacyStringFromValueExpression:expression];
  150. break;
  151. }
  152. default: {
  153. return expression.inputString;
  154. break;
  155. }
  156. }
  157. }
  158. -(NSString *) legacyStringFromOperationExpression:(SA_DiceExpression *)expression {
  159. if (expression.operator == SA_DiceExpressionOperator_MINUS &&
  160. expression.leftOperand == nil) {
  161. // Check to see if the term is a negation operation.
  162. return [@[ [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS],
  163. [self legacyStringFromIntermediaryExpression:expression.rightOperand]
  164. ] componentsJoinedByString:@""];
  165. } else if (expression.operator == SA_DiceExpressionOperator_MINUS ||
  166. expression.operator == SA_DiceExpressionOperator_PLUS ||
  167. expression.operator == SA_DiceExpressionOperator_TIMES) {
  168. // Check to see if the term is an addition, subtraction, or
  169. // multiplication operation.
  170. return [@[ [self legacyStringFromIntermediaryExpression:expression.leftOperand],
  171. [SA_DiceFormatter canonicalRepresentationForOperator:expression.operator],
  172. [self legacyStringFromIntermediaryExpression:expression.rightOperand]
  173. ] componentsJoinedByString:@" "];
  174. } else {
  175. // If the operator is not one of the supported operators, default to
  176. // outputting the input string.
  177. return expression.inputString;
  178. }
  179. }
  180. -(NSString *) legacyStringFromRollCommandExpression:(SA_DiceExpression *)expression {
  181. /*
  182. In legacy behavior, we print the result of roll commands with the rolls
  183. generated by the roll command. If a roll command generates a roll-related
  184. error (any of the errors that begin with DIE_), we print “ERROR” in place
  185. of a result.
  186. Legacy behavior assumes support for roll-and-sum only, so we do not need
  187. to adjust the output format for different roll commands.
  188. */
  189. return [NSString stringWithFormat:@"%@%@%@ < %@%@ >",
  190. [self legacyStringFromIntermediaryExpression:expression.dieCount],
  191. [SA_DiceFormatter canonicalRepresentationForRollCommandDelimiter:expression.rollCommand],
  192. [self legacyStringFromIntermediaryExpression:expression.dieSize],
  193. ((expression.rolls != nil) ?
  194. [NSString stringWithFormat:@"%@ = ",
  195. [(expression.dieType == SA_DiceExpressionDice_FUDGE ?
  196. [self formattedFudgeRolls:expression.rolls] :
  197. expression.rolls
  198. ) componentsJoinedByString:@" "]] :
  199. @""),
  200. (expression.result ?: @"ERROR")];
  201. }
  202. -(NSArray *) formattedFudgeRolls:(NSArray <NSNumber *> *)rolls {
  203. static NSDictionary *fudgeDieRollRepresentations;
  204. static dispatch_once_t onceToken;
  205. dispatch_once(&onceToken, ^{
  206. fudgeDieRollRepresentations = @{ @(-1): [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS],
  207. @(0): @"0",
  208. @(1): [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_PLUS]
  209. };
  210. });
  211. return [rolls map:^NSString *(NSNumber *roll) {
  212. return fudgeDieRollRepresentations[roll];
  213. }];
  214. }
  215. -(NSString *) legacyStringFromRollModifierExpression:(SA_DiceExpression *)expression {
  216. /*
  217. In legacy behavior, we print the result of roll modifiers with the rolls
  218. generated by the roll command, plus the modifications. If a roll modifier
  219. generates an error, we print “ERROR” in place of any of the components.
  220. Legacy behavior assumes support for the ‘keep’ modifier only, so we do not
  221. need to adjust the output format for different roll modifiers.
  222. */
  223. NSUInteger keptHowMany = expression.rightOperand.result.unsignedIntegerValue;
  224. return [NSString stringWithFormat:@"%@%@%@%@%@ < %@ less %@ leaves %@ = %@ >",
  225. [self legacyStringFromIntermediaryExpression:expression.leftOperand.dieCount],
  226. [SA_DiceFormatter canonicalRepresentationForRollCommandDelimiter:expression.leftOperand.rollCommand],
  227. [self legacyStringFromIntermediaryExpression:expression.leftOperand.dieSize],
  228. [SA_DiceFormatter canonicalRepresentationForRollModifierDelimiter:expression.rollModifier],
  229. expression.rightOperand.result,
  230. ((expression.leftOperand.rolls != nil) ?
  231. [(expression.leftOperand.dieType == SA_DiceExpressionDice_FUDGE ?
  232. [self formattedFudgeRolls:expression.rolls] :
  233. expression.leftOperand.rolls
  234. ) componentsJoinedByString:@" "] :
  235. @""),
  236. [(expression.leftOperand.dieType == SA_DiceExpressionDice_FUDGE ?
  237. [self formattedFudgeRolls:[expression.rolls subarrayWithRange:NSRangeMake(keptHowMany, expression.rolls.count - keptHowMany)]] :
  238. [expression.rolls subarrayWithRange:NSRangeMake(keptHowMany, expression.rolls.count - keptHowMany)]
  239. ) componentsJoinedByString:@" "],
  240. [(expression.leftOperand.dieType == SA_DiceExpressionDice_FUDGE ?
  241. [self formattedFudgeRolls:[expression.rolls subarrayWithRange:NSRangeMake(0, keptHowMany)]] :
  242. [expression.rolls subarrayWithRange:NSRangeMake(0, keptHowMany)]
  243. ) componentsJoinedByString:@" "],
  244. (expression.result ?: @"ERROR")];
  245. }
  246. -(NSString *) legacyStringFromValueExpression:(SA_DiceExpression *)expression {
  247. if ([expression.inputString.lowercaseString isEqualToString:@"f"]) {
  248. return @"F";
  249. } else {
  250. // We use the value for the ‘value’ property and not the ‘result’ property
  251. // because they should be the same, and the ‘result’ property might not
  252. // have a value (if the expression was not evaluated); this saves us
  253. // having to compare it against nil, and saves code.
  254. return [expression.value stringValue];
  255. }
  256. }
  257. /**********************************************/
  258. #pragma mark - “Simple” behavior implementation
  259. /**********************************************/
  260. -(NSString *) simpleStringFromExpression:(SA_DiceExpression *)expression {
  261. NSString *formattedString = [NSString stringWithFormat:@"%@",
  262. (expression.result ?: @"ERROR")];
  263. // Make all instances of the minus sign be represented with the proper,
  264. // canonical minus sign.
  265. return [SA_DiceFormatter rectifyMinusSignInString:formattedString];
  266. }
  267. /****************************/
  268. #pragma mark - Helper methods
  269. /****************************/
  270. +(NSString *) rectifyMinusSignInString:(NSString *)aString {
  271. NSMutableString* sameStringButMutable = aString.mutableCopy;
  272. NSString *validMinusSignCharacters = [SA_DiceFormatter stringFormatRules][SA_DB_VALID_CHARACTERS][SA_DB_VALID_OPERATOR_CHARACTERS][NSStringFromSA_DiceExpressionOperator(SA_DiceExpressionOperator_MINUS)];
  273. [validMinusSignCharacters enumerateSubstringsInRange:NSRangeMake(0, validMinusSignCharacters.length)
  274. options:NSStringEnumerationByComposedCharacterSequences
  275. usingBlock:^(NSString *aValidMinusSignCharacter,
  276. NSRange characterRange,
  277. NSRange enclosingRange,
  278. BOOL *stop) {
  279. [sameStringButMutable replaceOccurrencesOfString:aValidMinusSignCharacter
  280. withString:[SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS]
  281. options:NSLiteralSearch
  282. range:NSRangeMake(0, sameStringButMutable.length)];
  283. }];
  284. return [sameStringButMutable copy];
  285. }
  286. +(void) loadErrorDescriptions {
  287. NSString* errorDescriptionsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"SA_DB_ErrorDescriptions"
  288. ofType:@"plist"];
  289. _errorDescriptions = [NSDictionary dictionaryWithContentsOfFile:errorDescriptionsPath];
  290. if (!_errorDescriptions) {
  291. NSLog(@"Could not load error descriptions!");
  292. }
  293. }
  294. +(NSString *) descriptionForErrors:(NSUInteger)errorBitMask {
  295. if (_errorDescriptions == nil) {
  296. [SA_DiceFormatter loadErrorDescriptions];
  297. }
  298. NSMutableArray <NSString *> *errorDescriptions = [NSMutableArray array];
  299. for (int i = 0; i <= 18; i++) {
  300. if ((errorBitMask & 1 << i) == 0) continue;
  301. NSString *errorName = NSStringFromSA_DiceExpressionError((SA_DiceExpressionError) 1 << i);
  302. [errorDescriptions addObject:(_errorDescriptions[errorName] ?: errorName)];
  303. }
  304. return [errorDescriptions componentsJoinedByString:@" / "];
  305. }
  306. +(void) loadStringFormatRules {
  307. NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME
  308. ofType:@"plist"];
  309. _stringFormatRules = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath];
  310. if (!_stringFormatRules) {
  311. NSLog(@"Could not load string format rules!");
  312. }
  313. }
  314. +(NSString *) canonicalRepresentationForOperator:(SA_DiceExpressionOperator)operator {
  315. return [SA_DiceFormatter canonicalOperatorRepresentations][NSStringFromSA_DiceExpressionOperator(operator)];
  316. }
  317. +(NSDictionary *) canonicalOperatorRepresentations {
  318. return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS];
  319. }
  320. +(NSString *) canonicalRepresentationForRollCommandDelimiter:(SA_DiceExpressionRollCommand)command {
  321. return [SA_DiceFormatter canonicalRollCommandDelimiterRepresentations][NSStringFromSA_DiceExpressionRollCommand(command)];
  322. }
  323. +(NSDictionary *) canonicalRollCommandDelimiterRepresentations {
  324. return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATIONS];
  325. }
  326. +(NSString *) canonicalRepresentationForRollModifierDelimiter:(SA_DiceExpressionRollModifier)modifier {
  327. return [SA_DiceFormatter canonicalRollModifierDelimiterRepresentations][NSStringFromSA_DiceExpressionRollModifier(modifier)];
  328. }
  329. +(NSDictionary *) canonicalRollModifierDelimiterRepresentations {
  330. return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_ROLL_MODIFIER_DELIMITER_REPRESENTATIONS];
  331. }
  332. @end