A set of classes for parsing, evaluating, and formatting die roll strings.
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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)
  118. ? @" [ERROR: %@]"
  119. : @" [ERRORS: %@]"),
  120. [SA_DiceFormatter descriptionForErrors:expression.errorBitMask]];
  121. }
  122. // Make all instances of the minus sign be represented with the proper,
  123. // canonical minus sign.
  124. return [SA_DiceFormatter rectifyMinusSignInString:formattedString];
  125. }
  126. -(NSString *) legacyStringFromIntermediaryExpression:(SA_DiceExpression *)expression {
  127. /*
  128. In legacy behavior, we do not print the results of intermediate terms in
  129. the expression tree (since the legacy output format was designed for
  130. expressions generated by a parser that does not support parentheses,
  131. doing so would not make sense anyway).
  132. The exception is roll commands, where the result of a roll-and-sum command
  133. is printed along with the rolls.
  134. For this reasons, when we recursively retrieve the string representations
  135. of sub-expressions, we call this method, not -[legacyStringFromExpression:].
  136. */
  137. switch (expression.type) {
  138. case SA_DiceExpressionTerm_OPERATION: {
  139. return [self legacyStringFromOperationExpression:expression];
  140. break;
  141. }
  142. case SA_DiceExpressionTerm_ROLL_COMMAND: {
  143. return [self legacyStringFromRollCommandExpression:expression];
  144. break;
  145. }
  146. case SA_DiceExpressionTerm_ROLL_MODIFIER: {
  147. return [self legacyStringFromRollModifierExpression:expression];
  148. break;
  149. }
  150. case SA_DiceExpressionTerm_VALUE: {
  151. return [self legacyStringFromValueExpression:expression];
  152. break;
  153. }
  154. default: {
  155. return expression.inputString;
  156. break;
  157. }
  158. }
  159. }
  160. -(NSString *) legacyStringFromOperationExpression:(SA_DiceExpression *)expression {
  161. if (expression.operator == SA_DiceExpressionOperator_MINUS &&
  162. expression.leftOperand == nil) {
  163. // Check to see if the term is a negation operation.
  164. return [@[ [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS],
  165. [self legacyStringFromIntermediaryExpression:expression.rightOperand]
  166. ] componentsJoinedByString:@""];
  167. } else if (expression.operator == SA_DiceExpressionOperator_MINUS ||
  168. expression.operator == SA_DiceExpressionOperator_PLUS ||
  169. expression.operator == SA_DiceExpressionOperator_TIMES) {
  170. // Check to see if the term is an addition, subtraction, or
  171. // multiplication operation.
  172. return [@[ [self legacyStringFromIntermediaryExpression:expression.leftOperand],
  173. [SA_DiceFormatter canonicalRepresentationForOperator:expression.operator],
  174. [self legacyStringFromIntermediaryExpression:expression.rightOperand]
  175. ] componentsJoinedByString:@" "];
  176. } else {
  177. // If the operator is not one of the supported operators, default to
  178. // outputting the input string.
  179. return expression.inputString;
  180. }
  181. }
  182. -(NSString *) legacyStringFromRollCommandExpression:(SA_DiceExpression *)expression {
  183. /*
  184. In legacy behavior, we print the result of roll commands with the rolls
  185. generated by the roll command. If a roll command generates a roll-related
  186. error (any of the errors that begin with DIE_), we print “ERROR” in place
  187. of a result.
  188. Legacy behavior assumes support for roll-and-sum only, so we do not need
  189. to adjust the output format for different roll commands.
  190. */
  191. return [NSString stringWithFormat:@"%@%@%@ < %@%@ >",
  192. [self legacyStringFromIntermediaryExpression:expression.dieCount],
  193. [SA_DiceFormatter canonicalRepresentationForRollCommandDelimiter:expression.rollCommand],
  194. [self legacyStringFromIntermediaryExpression:expression.dieSize],
  195. ((expression.rolls != nil) ?
  196. [NSString stringWithFormat:@"%@ = ",
  197. [(expression.dieType == SA_DiceExpressionDice_FUDGE ?
  198. [self formattedFudgeRolls:expression.rolls] :
  199. expression.rolls
  200. ) componentsJoinedByString:@" "]] :
  201. @""),
  202. (expression.result ?: @"ERROR")];
  203. }
  204. -(NSArray *) formattedFudgeRolls:(NSArray <NSNumber *> *)rolls {
  205. static NSDictionary *fudgeDieRollRepresentations;
  206. static dispatch_once_t onceToken;
  207. dispatch_once(&onceToken, ^{
  208. fudgeDieRollRepresentations = @{ @(-1): [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS],
  209. @(0): @"0",
  210. @(1): [SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_PLUS]
  211. };
  212. });
  213. return [rolls map:^NSString *(NSNumber *roll) {
  214. return fudgeDieRollRepresentations[roll];
  215. }];
  216. }
  217. -(NSString *) legacyStringFromRollModifierExpression:(SA_DiceExpression *)expression {
  218. /*
  219. In legacy behavior, we print the result of roll modifiers with the rolls
  220. generated by the roll command, plus the modifications. If a roll modifier
  221. generates an error, we print “ERROR” in place of any of the components.
  222. Legacy behavior assumes support for the ‘keep’ modifier only, so we do not
  223. need to adjust the output format for different roll modifiers.
  224. */
  225. NSUInteger keptHowMany = expression.rightOperand.result.unsignedIntegerValue;
  226. return [NSString stringWithFormat:@"%@%@%@%@%@ < %@ less %@ leaves %@ = %@ >",
  227. [self legacyStringFromIntermediaryExpression:expression.leftOperand.dieCount],
  228. [SA_DiceFormatter canonicalRepresentationForRollCommandDelimiter:expression.leftOperand.rollCommand],
  229. [self legacyStringFromIntermediaryExpression:expression.leftOperand.dieSize],
  230. [SA_DiceFormatter canonicalRepresentationForRollModifierDelimiter:expression.rollModifier],
  231. expression.rightOperand.result,
  232. ((expression.leftOperand.rolls != nil) ?
  233. [(expression.leftOperand.dieType == SA_DiceExpressionDice_FUDGE ?
  234. [self formattedFudgeRolls:expression.rolls] :
  235. expression.leftOperand.rolls
  236. ) componentsJoinedByString:@" "] :
  237. @""),
  238. [(expression.leftOperand.dieType == SA_DiceExpressionDice_FUDGE ?
  239. [self formattedFudgeRolls:[expression.rolls subarrayWithRange:NSRangeMake(keptHowMany, expression.rolls.count - keptHowMany)]] :
  240. [expression.rolls subarrayWithRange:NSRangeMake(keptHowMany, expression.rolls.count - keptHowMany)]
  241. ) componentsJoinedByString:@" "],
  242. [(expression.leftOperand.dieType == SA_DiceExpressionDice_FUDGE ?
  243. [self formattedFudgeRolls:[expression.rolls subarrayWithRange:NSRangeMake(0, keptHowMany)]] :
  244. [expression.rolls subarrayWithRange:NSRangeMake(0, keptHowMany)]
  245. ) componentsJoinedByString:@" "],
  246. (expression.result ?: @"ERROR")];
  247. }
  248. -(NSString *) legacyStringFromValueExpression:(SA_DiceExpression *)expression {
  249. if ([expression.inputString.lowercaseString isEqualToString:@"f"]) {
  250. return @"F";
  251. } else {
  252. // We use the value for the ‘value’ property and not the ‘result’ property
  253. // because they should be the same, and the ‘result’ property might not
  254. // have a value (if the expression was not evaluated); this saves us
  255. // having to compare it against nil, and saves code.
  256. return [expression.value stringValue];
  257. }
  258. }
  259. /**********************************************/
  260. #pragma mark - “Simple” behavior implementation
  261. /**********************************************/
  262. -(NSString *) simpleStringFromExpression:(SA_DiceExpression *)expression {
  263. NSString *formattedString = [NSString stringWithFormat:@"%@",
  264. (expression.result ?: @"ERROR")];
  265. // Make all instances of the minus sign be represented with the proper,
  266. // canonical minus sign.
  267. return [SA_DiceFormatter rectifyMinusSignInString:formattedString];
  268. }
  269. /****************************/
  270. #pragma mark - Helper methods
  271. /****************************/
  272. +(NSString *) rectifyMinusSignInString:(NSString *)aString {
  273. NSMutableString* sameStringButMutable = aString.mutableCopy;
  274. NSString *validMinusSignCharacters = [SA_DiceFormatter stringFormatRules][SA_DB_VALID_CHARACTERS][SA_DB_VALID_OPERATOR_CHARACTERS][NSStringFromSA_DiceExpressionOperator(SA_DiceExpressionOperator_MINUS)];
  275. [validMinusSignCharacters enumerateSubstringsInRange:NSRangeMake(0, validMinusSignCharacters.length)
  276. options:NSStringEnumerationByComposedCharacterSequences
  277. usingBlock:^(NSString *aValidMinusSignCharacter,
  278. NSRange characterRange,
  279. NSRange enclosingRange,
  280. BOOL *stop) {
  281. [sameStringButMutable replaceOccurrencesOfString:aValidMinusSignCharacter
  282. withString:[SA_DiceFormatter canonicalRepresentationForOperator:SA_DiceExpressionOperator_MINUS]
  283. options:NSLiteralSearch
  284. range:NSRangeMake(0, sameStringButMutable.length)];
  285. }];
  286. return [sameStringButMutable copy];
  287. }
  288. +(void) loadErrorDescriptions {
  289. NSString* errorDescriptionsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"SA_DB_ErrorDescriptions"
  290. ofType:@"plist"];
  291. _errorDescriptions = [NSDictionary dictionaryWithContentsOfFile:errorDescriptionsPath];
  292. if (!_errorDescriptions) {
  293. NSLog(@"Could not load error descriptions!");
  294. }
  295. }
  296. +(NSString *) descriptionForErrors:(NSUInteger)errorBitMask {
  297. if (_errorDescriptions == nil) {
  298. [SA_DiceFormatter loadErrorDescriptions];
  299. }
  300. NSMutableArray <NSString *> *errorDescriptions = [NSMutableArray array];
  301. for (int i = 0; i <= 19; i++) {
  302. if ((errorBitMask & (1 << i)) == 0)
  303. continue;
  304. NSString *errorName = NSStringFromSA_DiceExpressionError((SA_DiceExpressionError) (1 << i));
  305. [errorDescriptions addObject:(_errorDescriptions[errorName] ?: errorName)];
  306. }
  307. return [errorDescriptions componentsJoinedByString:@" / "];
  308. }
  309. +(void) loadStringFormatRules {
  310. NSString *stringFormatRulesPath = [[NSBundle bundleForClass:[self class]] pathForResource:SA_DB_STRING_FORMAT_RULES_PLIST_NAME
  311. ofType:@"plist"];
  312. _stringFormatRules = [NSDictionary dictionaryWithContentsOfFile:stringFormatRulesPath];
  313. if (!_stringFormatRules) {
  314. NSLog(@"Could not load string format rules!");
  315. }
  316. }
  317. +(NSString *) canonicalRepresentationForOperator:(SA_DiceExpressionOperator)operator {
  318. return [SA_DiceFormatter canonicalOperatorRepresentations][NSStringFromSA_DiceExpressionOperator(operator)];
  319. }
  320. +(NSDictionary *) canonicalOperatorRepresentations {
  321. return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_OPERATOR_REPRESENTATIONS];
  322. }
  323. +(NSString *) canonicalRepresentationForRollCommandDelimiter:(SA_DiceExpressionRollCommand)command {
  324. return [SA_DiceFormatter canonicalRollCommandDelimiterRepresentations][NSStringFromSA_DiceExpressionRollCommand(command)];
  325. }
  326. +(NSDictionary *) canonicalRollCommandDelimiterRepresentations {
  327. return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_ROLL_COMMAND_DELIMITER_REPRESENTATIONS];
  328. }
  329. +(NSString *) canonicalRepresentationForRollModifierDelimiter:(SA_DiceExpressionRollModifier)modifier {
  330. return [SA_DiceFormatter canonicalRollModifierDelimiterRepresentations][NSStringFromSA_DiceExpressionRollModifier(modifier)];
  331. }
  332. +(NSDictionary *) canonicalRollModifierDelimiterRepresentations {
  333. return [SA_DiceFormatter stringFormatRules][SA_DB_CANONICAL_REPRESENTATIONS][SA_DB_CANONICAL_ROLL_MODIFIER_DELIMITER_REPRESENTATIONS];
  334. }
  335. @end