asweigart

wizcoin.py

Sep 16th, 2018
142
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. """
  2. WizCoin is a class to represent a quantity of coins in a wizard currency.
  3.  
  4. In this currency, there are knuts, sickles (worth 29 knuts), and galleons
  5. (worth 17 sickles or 493 knuts).
  6. """
  7.  
  8. __version__ = '0.0.1'
  9.  
  10. import copy
  11. import operator
  12.  
  13. # Constants used in this module:
  14. KNUTS_PER_SICKLE = 29
  15. SICKLES_PER_GALLEON = 17
  16. KNUTS_PER_GALLEON = SICKLES_PER_GALLEON * KNUTS_PER_SICKLE
  17.  
  18.  
  19. class WizCoinException(Exception):
  20.     """Exceptions of this class are raised by the wizcoin module for incorrect
  21.    use of the module. If wizcoin is the source of any other raised exceptions,
  22.    assume that it is caused by a bug in the module instead of misuse."""
  23.     pass
  24.  
  25.  
  26. class CoinBag:
  27.     """CoinBag objects represent an amount of coins, not money. They cannot
  28.    have half a coin, or a negative number of coins."""
  29.  
  30.     issuer = 'gb' # The ISO 2-letter country code of who issues this currency.
  31.  
  32.     def __init__(self, galleons=0, sickles=0, knuts=0):
  33.         """Create a new CoinBag object with galleons, sickles, and knuts."""
  34.         self.galleons = galleons
  35.         self.sickles = sickles
  36.         self.knuts = knuts
  37.  
  38.  
  39.     @property
  40.     def galleons(self):
  41.         """The number of galleons in the CoinBag."""
  42.         return self._galleons
  43.  
  44.  
  45.     @galleons.setter
  46.     def galleons(self, value):
  47.         if not isinstance(value, int) or value < 0:
  48.             raise WizCoinException('galleons attr must be a positive int')
  49.         self._galleons = value
  50.  
  51.  
  52.     @galleons.deleter
  53.     def galleons(self):
  54.         self._galleons = 0
  55.  
  56.  
  57.     @property
  58.     def sickles(self):
  59.         """The number of sickles in the CoinBag."""
  60.         return self._sickles
  61.  
  62.  
  63.     @sickles.setter
  64.     def sickles(self, value):
  65.         if not isinstance(value, int) or value < 0:
  66.             raise WizCoinException('sickles attr must be a positive int')
  67.         self._sickles = value
  68.  
  69.  
  70.     @sickles.deleter
  71.     def sickles(self):
  72.         self._sickles = 0
  73.  
  74.  
  75.     @property
  76.     def knuts(self):
  77.         """The number of knuts in the CoinBag."""
  78.         return self._knuts
  79.  
  80.  
  81.     @knuts.setter
  82.     def knuts(self, value):
  83.         if not isinstance(value, int) or value < 0  :
  84.             raise WizCoinException('knuts attr must be a positive int')
  85.         self._knuts = value
  86.  
  87.  
  88.     @knuts.deleter
  89.     def knuts(self):
  90.         self._knuts = 0
  91.  
  92.  
  93.     @property
  94.     def value(self):
  95.         """The value (in knuts) of all the coins in this CoinBag."""
  96.         return (self._galleons * KNUTS_PER_GALLEON) + (self._sickles * KNUTS_PER_SICKLE) + (self._knuts)
  97.  
  98.  
  99.     def convertToGalleons(self):
  100.         """Modifies the CoinBag in-place, converting knuts and sickles to
  101.        galleons. There may knuts and sickles leftover as change."""
  102.  
  103.         # Convert knuts to sickles, then sickles to galleons.
  104.         self._sickles += self._knuts // KNUTS_PER_SICKLE
  105.         self._knuts %= KNUTS_PER_SICKLE # Knuts may be remaining as change.
  106.         self._galleons += self._sickles // SICKLES_PER_GALLEON
  107.         self._sickles %= SICKLES_PER_GALLEON # Sickles might remain as change.
  108.  
  109.  
  110.     def convertToSickles(self):
  111.         """Modifies the CoinBag object in-place, converting knuts and
  112.        galleons to sickles. There may knuts leftover as change."""
  113.         self._sickles += (self._galleons * SICKLES_PER_GALLEON) + (self._knuts // KNUTS_PER_SICKLE)
  114.         self._knuts %= KNUTS_PER_SICKLE # Knuts might remain as change.
  115.         self._galleons = 0
  116.  
  117.  
  118.     def convertToKnuts(self):
  119.         """Modifies the CoinBag object in-place, converting galleons and
  120.        sickles to knuts."""
  121.         self._knuts += (self._galleons * KNUTS_PER_GALLEON) + (self._sickles * KNUTS_PER_SICKLE)
  122.         self._galleons = 0
  123.         self._sickles = 0
  124.  
  125.  
  126.     def __repr__(self):
  127.         """Returns a string representation of this CoinBag object that can be
  128.        fed into the interactive shell to make an identical CoinBag object."""
  129.         className = type(self).__name__
  130.         return '%s(galleons=%s, sickles=%s, knuts=%s)' % (className, self._galleons, self._sickles, self._knuts)
  131.  
  132.  
  133.     def __len__(self):
  134.         """Returns the number of coins in this CoinBag."""
  135.         return self._galleons + self._sickles + self._knuts
  136.  
  137.  
  138.     def __copy__(self):
  139.         """Returns a new, duplicate CoinBag object of this CoinBag."""
  140.         return CoinBag(self._galleons, self._sickles, self._knuts)
  141.  
  142.  
  143.     def __deepcopy__(self, memo):
  144.         """Returns a new, duplicate CoinBag object of this CoinBag. This
  145.        method reuses __copy__() since CoinBags don't need deep copies."""
  146.         return self.__copy__()
  147.  
  148.  
  149.     def __str__(self):
  150.         """Returns a string representation of the CoinBag object, formatted
  151.        like '2g,5s,10k' for a CoinBag of 2 galleons, 5 sickles, 10 knuts."""
  152.         return '%sg,%ss,%sk' % (self._galleons, self._sickles, self._knuts)
  153.  
  154.  
  155.     def __int__(self):
  156.         """Returns the value of the coins in this CoinBag as an int."""
  157.         return self.value
  158.  
  159.  
  160.     def __float__(self):
  161.         """Returns the value of the coins in this CoinBag as a float."""
  162.         return float(self.value)
  163.  
  164.  
  165.     def __bool__(self):
  166.         """Returns the Boolean value of the CoinBag."""
  167.         return not (self._galleons == 0 and self._sickles == 0 and self._knuts == 0)
  168.  
  169.  
  170.     @classmethod
  171.     def fromStr(cls, coinStr):
  172.         """An alternative constructor that gets the coin amounts from
  173.        `coinStr`, which is formatted like '2g,5s,10k'."""
  174.         try:
  175.             if coinStr == '':
  176.                 return cls(galleons=0, sickles=0, knuts=0)
  177.  
  178.             gTotal = 0
  179.             sTotal = 0
  180.             kTotal = 0
  181.  
  182.             for coinStrPart in coinStr.split(','):
  183.                 if coinStrPart.endswith('g'):
  184.                     gTotal += int(coinStrPart[:-1])
  185.                 elif coinStrPart.endswith('s'):
  186.                     sTotal += int(coinStrPart[:-1])
  187.                 elif coinStrPart.endswith('k'):
  188.                     kTotal += int(coinStrPart[:-1])
  189.                 else:
  190.                     raise Exception()
  191.         except:
  192.             raise WizCoinException('coinStr has an invalid format')
  193.         return cls(galleons=gTotal, sickles=sTotal, knuts=kTotal)
  194.  
  195.  
  196.     @classmethod
  197.     def isEuropeanCurrency(cls):
  198.         """A helper method that returns if this currency is used in Europe."""
  199.         return cls.issuer in {'ad', 'al', 'am', 'at', 'ba', 'be', 'bg', 'by', 'ch', 'cy', 'cz', 'de', 'dk', 'ee', 'es', 'fi', 'fo', 'fr', 'gb', 'ge', 'gi', 'gr', 'hr', 'hu', 'ie', 'im', 'is', 'it', 'li', 'lt', 'lu', 'lv', 'mc', 'md', 'me', 'mk', 'mt', 'nl', 'no', 'pl', 'po', 'pt', 'ro', 'rs', 'ru', 'se', 'si', 'sk', 'sm', 'tr', 'ua', 'va'}
  200.  
  201.  
  202.     @staticmethod
  203.     def _isCoinBagType(obj): # This should be a module-level function.
  204.         """A helper function that returns True if `obj` has `galleons`,
  205.        `sickles`, `knuts`, and `value` attributes, otherwise returns
  206.        False."""
  207.         return hasattr(obj, 'galleons') and hasattr(obj, 'sickles') and hasattr(obj, 'galleons') and hasattr(obj, 'value')
  208.  
  209.  
  210.     # Overloading comparison operators:
  211.     def _comparisonOperatorHelper(self, operatorFunc, other):
  212.         """A helper method that carries out a comparison operation."""
  213.         if CoinBag._isCoinBagType(other):
  214.             # Compare this CoinBag's value with another CoinBag's value.
  215.             return operatorFunc(self.value, other.value)
  216.         elif isinstance(other, (int, float)):
  217.             # Compare this CoinBag's value with an int or float.
  218.             return operatorFunc(self.value, other)
  219.         elif operatorFunc == operator.eq:
  220.             return False # Not equal to all non CoinBag/int/float values.
  221.         elif operatorFunc == operator.ne:
  222.             return True # Not equal to all non CoinBag/int/float values.
  223.         else:
  224.             # Can't compare with whatever data type `other` is.
  225.             raise WizCoinException("'%s' not supported between instances of '%s' and '%s'" % (operatorFunc.__name__, self.__class__.__name__, other.__class__.__name__))
  226.  
  227.  
  228.     def __eq__(self, other):
  229.         """Overloads the == operator to compare CoinBag objects with ints,
  230.        floats, and other CoinBag objects."""
  231.         return self._comparisonOperatorHelper(operator.eq, other)
  232.  
  233.  
  234.     def __ne__(self, other):
  235.         """Overloads the != operator to compare CoinBag objects with ints,
  236.        floats, and other CoinBag objects."""
  237.         return self._comparisonOperatorHelper(operator.ne, other)
  238.  
  239.  
  240.     def __lt__(self, other):
  241.         """Overloads the < operator to compare CoinBag objects with ints,
  242.        floats, and other CoinBag objects."""
  243.         return self._comparisonOperatorHelper(operator.lt, other)
  244.  
  245.  
  246.     def __le__(self, other):
  247.         """Overloads the <= operator to compare CoinBag objects with ints,
  248.        floats, and other CoinBag objects."""
  249.         return self._comparisonOperatorHelper(operator.le, other)
  250.  
  251.  
  252.     def __gt__(self, other):
  253.         """Overloads the > operator to compare CoinBag objects with ints,
  254.        floats, and other CoinBag objects."""
  255.         return self._comparisonOperatorHelper(operator.gt, other)
  256.  
  257.  
  258.     def __ge__(self, other):
  259.         """Overloads the >= operator to compare CoinBag objects with ints,
  260.        floats, and other CoinBag objects."""
  261.         return self._comparisonOperatorHelper(operator.ge, other)
  262.  
  263.  
  264.     # Overloading math operators:
  265.     def __mul__(self, other):
  266.         """Overloads the * operator to produce a new CoinBag object with the
  267.        product amount. `other` must be a positive int."""
  268.         if isinstance(other, int) and other >= 0:
  269.             return CoinBag(self._galleons * other,
  270.                            self._sickles * other,
  271.                            self._knuts * other)
  272.         else:
  273.             raise WizCoinException('%s objects can only multiply with positive ints' % (self.__class__.__name__))
  274.  
  275.  
  276.     def __rmul__(self, other):
  277.         """Overloads the * operator to produce a new CoinBag object with the
  278.        product amount. `other` must be a positive int."""
  279.         return self.__mul__(other) # * is commutative, reuse __mul__().
  280.  
  281.  
  282.     def __imul__(self, other):
  283.         """Overloads the * operator to modify a CoinBag object in-place with
  284.        the product amount. `other` must be a positive int."""
  285.         if isinstance(other, int) and other >= 0:
  286.             self._galleons *= other # In-place modification.
  287.             self._sickles *= other
  288.             self._knuts *= other
  289.         else:
  290.             raise WizCoinException('%s objects can only multiply with positive ints' % (self.__class__.__name__))
  291.         return self
  292.  
  293.  
  294.     def __add__(self, other):
  295.         """Overloads the + operator to produce a new CoinBag object with the
  296.        sum amount. `other` must be a CoinBag."""
  297.         if CoinBag._isCoinBagType(other):
  298.             return CoinBag(self._galleons + other.galleons,
  299.                            self._sickles + other.sickles,
  300.                            self._knuts + other.knuts)
  301.         else:
  302.             raise WizCoinException('%s objects can only add with other wizcoin.CoinBag objects' % (self.__class__.__name__))
  303.  
  304.  
  305.     def __iadd__(self, other):
  306.         """Overloads the += operator to modify this CoinBag in-place with the
  307.        sum amount. `other` must be a CoinBag."""
  308.         if CoinBag._isCoinBagType(other):
  309.             self._galleons += other.galleons # In-place modification.
  310.             self._sickles += other.sickles
  311.             self._knuts += other.knuts
  312.         else:
  313.             raise WizCoinException('%s objects can only add with other wizcoin.CoinBag objects' % (self.__class__.__name__))
  314.         return self
  315.  
  316.  
  317.     def __sub__(self, other):
  318.         """Overloads the - operator to produce a new CoinBag object with the
  319.        difference amount. `other` must be a CoinBag object with less than or
  320.        equal number of coins of each type as this CoinBag object."""
  321.         if CoinBag._isCoinBagType(other):
  322.             if self._galleons < other.galleons or self._sickles < other.sickles or self._knuts < other.knuts:
  323.                 raise WizCoinException('subtracting %s from %s would result in negative quantity of coins' % (other, self))
  324.             return CoinBag(self._galleons - other.galleons,
  325.                            self._sickles - other.sickles,
  326.                            self._knuts - other.knuts)
  327.         else:
  328.             raise WizCoinException('%s objects can only subtract with other wizcoin.CoinBag objects' % (self.__class__.__name__))
  329.  
  330.  
  331.     def __isub__(self, other):
  332.         """Overloads the -= operator to modify this CoinBag in-place with the
  333.        difference amount. `other` must be a CoinBag object with less than or
  334.        equal number of coins of each type as this CoinBag object."""
  335.         if CoinBag._isCoinBagType(other):
  336.             if self._galleons < other.galleons or self._sickles < other.sickles or self._knuts < other.knuts:
  337.                 raise WizCoinException('subtracting %s from %s would result in negative quantity of coins' % (other, self))
  338.             self._galleons -= other.galleons
  339.             self._sickles -= other.sickles
  340.             self._knuts -= other.knuts
  341.         else:
  342.             raise WizCoinException('%s objects can only subtract with other wizcoin.CoinBag objects' % (self.__class__.__name__))
  343.         return self
  344.  
  345.  
  346.     def __lshift__(self, other):
  347.         """Overloads the << operator to transfer all coins from the CoinBag on
  348.        the right side to the CoinBag on the left side."""
  349.         if not CoinBag._isCoinBagType(other):
  350.             raise WizCoinException('CoinBag can only use << on other CoinBag objects')
  351.  
  352.         self._galleons += other.galleons # Add to this CoinBag.
  353.         self._sickles += other.sickles
  354.         self._knuts += other.knuts
  355.         other.galleons = 0 # Empty the other CoinBag.
  356.         other.sickles = 0
  357.         other.knuts = 0
  358.  
  359.  
  360.     def __rshift__(self, other):
  361.         """Overloads the >> operator to transfer all coins from the CoinBag on
  362.        the left side to the CoinBag on the right side."""
  363.         if not CoinBag._isCoinBagType(other):
  364.             raise WizCoinException('CoinBag can only use >> on other CoinBag objects')
  365.  
  366.         other.galleons += self._galleons # Add to the other CoinBag.
  367.         other.sickles += self._sickles
  368.         other.knuts += self._knuts
  369.         self._galleons = 0 # Empty this CoinBag.
  370.         self._sickles = 0
  371.         self._knuts = 0
  372.  
  373.  
  374.     def __getitem__(self, idx):
  375.         """Overloads the [] operator to access what kind of coin is at index
  376.        `idx`. The order of coins is galleons, then sickles, then knutes."""
  377.         if idx >= len(self) or idx < -len(self):
  378.             raise WizCoinException('index out of range')
  379.         if idx < 0:
  380.             idx = len(self) + idx # Convert negative index to positive.
  381.  
  382.         if idx < self._galleons:
  383.             return 'galleon'
  384.         elif idx < self._galleons + self._sickles:
  385.             return 'sickle'
  386.         else:
  387.             return 'knut'
  388.  
  389.  
  390.     def __setitem__(self, idx, coinType):
  391.         """Overloads the [] operator to access what kind of coin is at index
  392.        `idx`. The order of coins is galleons, then sickles, then knutes."""
  393.         if coinType not in ('galleon', 'sickle', 'knut'):
  394.             raise WizCoinException("coinType must be one of 'galleon', 'sickle', or 'knut'")
  395.         try:
  396.             coin = self[idx]
  397.         except Exception as exc:
  398.             raise WizCoinException(str(exc))
  399.  
  400.         if coin == 'galleon':
  401.             self._galleons -= 1
  402.         elif coin == 'sickle':
  403.             self._sickles -= 1
  404.         elif coin == 'knut':
  405.             self._knuts -= 1
  406.  
  407.         # Add a coin of type `coinType`.
  408.         if coinType == 'galleon':
  409.             self._galleons += 1
  410.         elif coinType == 'sickle':
  411.             self._sickles += 1
  412.         elif coinType == 'knut':
  413.             self._knuts += 1
  414.  
  415.  
  416.     def __delitem__(self, idx):
  417.         """Overloads the [] operator to remove the kind of coin at index
  418.        `idx`."""
  419.         try:
  420.             coin = self[idx]
  421.         except Exception as exc:
  422.             raise WizCoinException(str(exc))
  423.  
  424.         if coin == 'galleon':
  425.             self._galleons -= 1
  426.         elif coin == 'sickle':
  427.             self._sickles -= 1
  428.         elif coin == 'knut':
  429.             self._knuts -= 1
  430.  
  431.  
  432.     def __iter__(self):
  433.         """Returns an iterator that iterates over the coins in this CoinBag.
  434.        The order of coins is galleons, then sickles, then knuts."""
  435.         return CoinBagIterator(self)
  436.  
  437.  
  438. class CoinBagIterator:
  439.     def __init__(self, coinBagObj):
  440.         """Creates an iterator for the given CoinBag object."""
  441.         self.nextIndex = 0
  442.         self.coinBagObj = coinBagObj
  443.  
  444.     def __next__(self):
  445.         """Returns the next coin from the CoinBag. The order of coins is
  446.        galleons, then sickles, then knuts."""
  447.         if self.nextIndex >= len(self.coinBagObj):
  448.             raise StopIteration
  449.  
  450.         nextCoin = self.coinBagObj[self.nextIndex]
  451.         self.nextIndex += 1
  452.         return nextCoin
  453.  
  454.  
  455. class CoinBagCollection:
  456.     def __init__(self, coinBags):
  457.         self.coinBags = tuple(coinBags)
  458.         self._origAmounts = tuple([copy.copy(bag) for bag in self.coinBags])
  459.  
  460.         for bag in self.coinBags:
  461.             if not CoinBag._isCoinBagType(bag):
  462.                 raise WizCoinException('all arguments to CoinBagCollection must be CoinBag objects')
  463.  
  464.  
  465.     def __enter__(self):
  466.         self.expectedTotal = sum([bag.value for bag in self.coinBags])
  467.         return tuple(self.coinBags)
  468.  
  469.  
  470.     def __exit__(self, excType, excValue, excTraceback):
  471.         total = sum([bag.value for bag in self.coinBags])
  472.         if total == self.expectedTotal and excType is None:
  473.             return # Everything is fine.
  474.  
  475.         # Reset bags to their original amounts.
  476.         for i, bag in enumerate(self.coinBags):
  477.             bag._galleons = self._origAmounts[i]._galleons
  478.             bag._sickles = self._origAmounts[i]._sickles
  479.             bag._knuts = self._origAmounts[i]._knuts
  480.  
  481.         if total != self.expectedTotal:
  482.             raise WizCoinException('expected total value (%s) does not match current total value (%s)' % (self.expectedTotal, total))
RAW Paste Data