2008年9月18日木曜日

酒飲み父ちゃんのアルコール量チェック (2) – Python で実装

酒飲み父ちゃんのアルコール量チェック – UML でクラス図の概略を作成」の続き。

 

具体例をシンプルに計算

前回、 クラス図の概略を作成した。はじめにこのクラス図は横に置いておき、具体例をシンプルに計算してみる。例えば、

今日、父ちゃんが「日本酒 : 1 合、焼酎 : 0.5 合、ウイスキー : 30 ml 」飲んだとする。それぞれのアルコール度数を「 15 度,  25 度,  40 度」とした場合、全部で日本酒で換算すると何合飲んだか?

ん~、暗算では無理。(+_+)

これを計算するには、すべての単位を ml に換算して、

# 日本酒 1 合のアルコール量
ALCOHOL_PER_SAKE_1GOU = 180 * 15 / 100

# 飲酒
sake    = 1 * 180 * 15 / 100        # 日本酒     : 1 合
shochu  = 0.5 * 180 * 25 / 100      # 焼酎       : 0.5 合
whiskey = 30 * 1 * 40 / 100         # ウイスキー : 30 ml

# 飲酒量を日本酒で考えると
print round((sake + shochu + whiskey) / ALCOHOL_PER_SAKE_1GOU, 2)

翌日に再度、同じように計算したい場合は、「飲酒」の変数に設定した計算式の中の値を変更。別の種類の酒を飲んだら、新しく変数を追加。日本酒換算ではなくて、焼酎換算にしたい場合は、最初の ALCOHOL_PER_SAKE_1GOU  の式を変更。新しい単位が追加された場合は、単位と ml への換算の式は頭の中に記憶しておくか、または別の管理方法を考えなくてはいけない。 この方法は、あくまでも一例を計算するだけ。

 

ちょっとまとめる

翌日もう少し入力しやすいように、変更がありそうなところをまとめておく。 単位 ・ml への変換やアルコールの度数の情報を一箇所で管理、アルコール量の計算で毎回度数を 100 で割るという手間をなくしたり。とりあえず、クラスを使わないで書くなら、

# 酒 : アルコール度数
LIQUOR_LIST = {u'日本酒'     : 15,
               u'ウイスキー' : 40,
               u'焼酎'       : 25}

# 単位 : 各単位は ml へ変換するときに乗算する値
UNIT_LIST = {'ml'   : 1,
             'l'    : 1000,
             u'合'  : 180}

def alcoholPer1Unit(liquor, unit):
    u""" 指定された酒の1単位当たりのアルコール量を返す """
    return UNIT_LIST[unit] * LIQUOR_LIST[liquor] / 100.0

def quantityOfAlcohol(liquor, val, unit):
    u""" アルコールの量を単位 ml で返す """
    return val * UNIT_LIST[unit] * LIQUOR_LIST[liquor] / 100.0

def totalQuantityOfAlcohol(drinksOfToday):
    u""" 今日の飲酒量からトータルのアルコール量を返す """
    return sum([quantityOfAlcohol(x[0], x[1], x[2]) for x in drinksOfToday])

def isOk(quantityOfAl, limit):
    u""" 一日の許容アルコール量を超えていないかチェックする """
    return u"!!! 飲みすぎ !!!" if quantityOfAl > limit else ""

def printResult(drinksOfToday):
    today = totalQuantityOfAlcohol(drinksOfToday)
    print u"%s に換算すると %.2f %s 飲んだ" % \
            (LIQUOR, today / alcoholPer1Unit(LIQUOR, UNIT), UNIT)
    print isOk(today, quantityOfAlcohol(LIQUOR, LIMIT, UNIT))

# 飲酒制限
LIQUOR = u'日本酒'; LIMIT = 2; UNIT = u'合'

# 今日の飲酒量
drinksOfToday = [(u'日本酒', 1, u'合'),
                 (u'焼酎' , 0.5, u'合'),
                 (u'ウイスキー', 30, u'ml')]
printResult(drinksOfToday)

 

あ~、もうこれでいいような気もしてきた ^^; UI を被せるにはまだ足りないけど、計算する分にはこれでいいか。

 

クラス図に基づいて

UML で概略書いたので、それに基づいて実装してみる。図とはちょっと違うけれど ^^;

import logging
from decimal import Decimal

class Liquor:
    u""" 酒 """
    def __init__(self, name, alcoholByPercentage):
        self.name = name            # 酒の名前
        # アルコールの含有量
        self.alcoholByPercentage = Decimal(str(alcoholByPercentage))

    def __str__(self):
        return name + " " + str(self.alcoholByPercentage)
    
    def setDefaultUnit(self, unit):
        u""" この酒で使われるデフォルトの単位を設定する。

        Drink#__call__ の呼出しに先立ち、単位を設定しておく。
        """
        self.unit = unit
        return self

class Quantity:
    u""" 量 """
    def __init__(self, number, unit):
        self.number = Decimal(str(number))      # 数値
        self.unit = unit                        # 単位

    def __cmp__(self, other):
        # 単位 ml に変換してから量を比較する
        return cmp(self.convertToMl(), other.convertToMl())

    def __str__(self):
        return "%.2f" % self.number + " " + self.unit.name + \
                " (%.2fml)" % self.unit.convertToMl(self.number)

    def convertToMl(self):
        u""" 量を単位 ml に変換して返す """
        return self.unit.convertToMl(self.number)
    
    def getUnit(self):
        u""" 単位を返す """
        return self.unit

class Unit:
    u""" 単位

    基準となる単位を ml とした。すべての単位は ml へと変換するときに
    乗算する値を持たなくてはいけない。
    """
    def __init__(self, name, valOfConvMl):
        self.name = name                # 単位名
        self.convertUnits = []          # 単位変換のリスト
        # 単位 ml へ変換するために乗算する値
        self.valOfConvMl = Decimal(str(valOfConvMl))  
        
    def convertToMl(self, number):
        u""" 指定された数をml へ変換する。 """
        return number * self.valOfConvMl

class Drink:
    u""" 飲酒 """
    def __init__(self, liquor, quantity=0):
        self.liquor = liquor            # 酒
        self.quantity = quantity        # 量
    
    def __str__(self):
        return u"%s, %-s" % \
                (self.liquor.name, self.quantity)
        
    def __call__(self, number):
        u""" 数値を指定し予め生成ておいた飲酒オブジェクトを元に新たに Drink
        オブジェクトを生成する。
        
        Drink オブジェクトを簡単に作るためのメソッド。この呼出しに先立ち、
        Liquor クラスだけが設定されていること。そのオブジェクトを使い、
        数値を設定するだけで、Drink オブジェクトが生成される。
        """
        assert self.liquor.unit is not None
        return Drink(self.liquor, Quantity(number, self.liquor.unit))

    def getAlcoholByMl(self):
        u""" 単位 ml でアルコールの量を返す """
        return self.getQuantityByMl() * self.getAlcoholPercentage()
        
    def getQuantityByMl(self):
        u""" 飲酒の量を単位 ml で返す """
        return self.quantity.convertToMl()

    def getAlcoholPercentage(self):
        u""" アルコールの割合を返す """
        return self.liquor.alcoholByPercentage / 100
    
    def getUnit(self):
        u""" 単位を返す """
        return self.quantity.getUnit()
    
    def getLiquorName(self):
        u""" 酒の名前を返す """
        return self.liquor.name
    
    def getAlcoholPerUnitByMl(self):
        u""" 1 単位当たりのアルコール量 を ml で返す """
        return self.getAlcoholByMl() / self.quantity.number

class DrinkList:
    u""" 飲酒のリスト """
    def __init__(self):
        self.drinks = []     # 飲酒のリスト

    def __str__(self):
        return u"--------- 飲酒リスト ---------\n" + \
            "\n".join(["%s" % drink for drink in self.drinks]) + "\n" + \
            "-"*30 + "\n" + \
            u"アルコール総量:%.2f ml" % self.getAlcoholByMl()
        
    def add(self, Drink):
        u""" 飲酒を飲酒のリストに追加する """
        self.drinks.append(Drink)
        return self
    
    def getAlcoholByMl(self):
        u""" 飲酒におけるアルコールの総量を単位 ml で返す """
        return sum([x.getAlcoholByMl() for x in self.drinks])

class AlcoholRestriction:
    u""" 飲酒制限

    ex. 「一日、日本酒で 2 合までなら飲んでいい」
    """
    def __init__(self, drink):
        self.drink = drink        # 上限となる飲酒
        
    def __str__(self):
        result = u"制限: 一日に %s で %s までなら飲んいいよ!" % \
                    (self.drink.getLiquorName(), self.drink.quantity)
        result += u"\n!!! 飲みすぎ!!!" if not self.permit() else ""
        result += u"\n%s %s に相当" % \
                    (self.drink.getLiquorName(),
                    Quantity(self.drinkList.getAlcoholByMl() / \
                                self.drink.getAlcoholPerUnitByMl(),
                            self.drink.getUnit()))
        return result
    
    def setDrinkList(self, drinkList):
        u""" 飲酒リストを設定 """
        self.drinkList = drinkList
    
    def getLimitOfAlcoholByMl(self):
        u""" 設定された飲酒の上限であるアルコール量を返す """
        return self.drink.getAlcoholByMl()
        
    def permit(self):
        u""" 指定された飲酒リストにおけるアルコールの量は許容されるか? """
        assert self.drinkList is not None
        if self.drinkList.getAlcoholByMl() <= self.getLimitOfAlcoholByMl():
                return True
        else: return False

# --- 酒-------------------------------------------------------
u""" 日本酒 """
LIQUOR_SAKE = Liquor(u"日本酒", 15)
u""" 焼酎 """
LIQUOR_SHOCHU = Liquor(u"焼酎", 25)
u""" ウイスキー """
LIQUOR_WHISKEY = Liquor(u"ウイスキー", 40)

# --- 単位 -----------------------------------------------------
# `ml' を量を比較する際の基準となる単位とする
u""" ml """
UNIT_ML = Unit(u"ml", 1)

u""" 合 """
UNIT_GOU = Unit(u"合", 180)
u""" 升 """
UNIT_SYOU = Unit(u"升", 1800)

# --- 量 -------------------------------------------------------
# ウイスキー・バーボンなどにおける特別な量の呼び方
u""" シングル """
QUANTITY_SINGLE = Quantity(30, UNIT_ML)
u""" ダブル """
QUANTITY_DOUBLE = Quantity(60, UNIT_ML)
u""" ジガー """
QUANTITY_JIGGER = Quantity(45, UNIT_ML)

# --- 飲酒 ------------------------------------------------------
u""" 飲酒オブジェクトを簡単に作成できるようにする。

ex. Liquor.DRINK_SAKE(2)      : 「酒を 2 合飲んだ」
    Liquor.DRINK_WHISKEY(100) : 「ウイスキーを 100 ml 飲んだ」
Drink#__call__() が呼出される。
"""

u""" 日本酒の飲酒を生成するためのオブジェクト
デフォルトの単位を「合」とする。
"""
DRINK_SAKE = Drink(LIQUOR_SAKE.setDefaultUnit(UNIT_GOU))
u""" 焼酎の飲酒を生成するためのオブジェクト
デフォルトの単位を「合」とする。
"""
DRINK_SHOCHU = Drink(LIQUOR_SHOCHU.setDefaultUnit(UNIT_GOU))
u""" ウイスキーの飲酒を生成するためのオブジェクト
デフォルトの単位を「ml」とした
"""
DRINK_WHISKEY = Drink(LIQUOR_WHISKEY.setDefaultUnit(UNIT_ML))


if __name__ == "__main__":
    # 飲酒は「日本酒で2合まで」という制約
    alcoholRestriction = AlcoholRestriction(DRINK_SAKE(2))
    # 飲酒リスト
    drinkList = DrinkList().add(
        DRINK_SAKE(1)).add(
        DRINK_SHOCHU(0.5)).add(                      # 焼酎 1 合
        Drink(LIQUOR_WHISKEY, QUANTITY_SINGLE))    # ウイスキーのシングル

    # 飲酒制限に飲酒リストを設定
    alcoholRestriction.setDrinkList(drinkList)

    # 飲酒リストを表示
    print "%s" % drinkList
    # 結果を表示
    print "%s" % alcoholRestriction

 

 

ユニットテスト

やってること単純だけれど、テストしながらじゃないと実装できない ^^;

import unittest
import Liquor
import logging
from decimal import Decimal

class TestLiquor(unittest.TestCase):

    def setUp(self): 
        # 単位
        self.unitGou = Liquor.Unit(u"合", 180)
        self.unitMl = Liquor.Unit(u"ml", 1)
        
        # 酒
        self.sake = Liquor.Liquor(u"焼酎", 25)
        self.whiskey = Liquor.Liquor(u"ウイスキー", 45)
        
        #--- 飲酒 ---
        # 焼酎を 1 合飲む
        self.d1 = Liquor.Drink(self.sake, Liquor.Quantity(1, self.unitGou))
        # ウイスキーを 100 ml 飲む
        self.d2 = Liquor.Drink(self.whiskey,
                                    Liquor.Quantity(100, self.unitMl))
        # 焼酎を 2 合飲む
        self.d3 = Liquor.Drink(self.sake, Liquor.Quantity(2, self.unitGou))
        # ウイスキーを 150 ml 飲む
        self.d4 = Liquor.Drink(self.whiskey,
                                    Liquor.Quantity(150, self.unitMl))

        # 飲酒リスト
        self.dl = Liquor.DrinkList()
        self.dl.add(self.d1).add(self.d2).add(self.d3).add(self.d4)
        
        self.dl2 = Liquor.DrinkList()
        self.dl2.add(self.d1)
        
        self.dl3 = Liquor.DrinkList()
        self.dl3.add(self.d3)
        
        # 飲酒制限
        self.alcoholRestriction = Liquor.AlcoholRestriction(
                                        Liquor.Drink(self.sake,
                                            Liquor.Quantity(2, self.unitGou)))
                                                        
    def tearDown(self): 
        pass
    
            
    def testAlcoholOfDrink(self):
        u""" 飲酒におけるアルコール量のテスト """
        # 焼酎を 1 合飲んだ場合、45 ml のアルコール
        self.assertEqual(45, self.d1.getAlcoholByMl())
            
    def testAlcoholOfDrinkList(self):
        u""" 飲酒リストのアルコール量をテスト """
        dl = Liquor.DrinkList()
        
        dl.add(self.d1)
        self.assertEqual(45, dl.getAlcoholByMl())

        dl.add(self.d2)
        self.assertEqual(90, dl.getAlcoholByMl())

        dl.add(self.d3)
        self.assertEqual(180, dl.getAlcoholByMl())

        dl.add(self.d4)
        self.assertEqual(Decimal("247.5"), dl.getAlcoholByMl())
        
        # 合で
        self.assertEqual(Decimal(str(1.375 * 180)), dl.getAlcoholByMl())
    
    def testQuantityOfDrink(self):
        u""" 1 合飲んだ焼酎の飲酒の量を ml で """
        self.assertEqual(45, self.d1.getAlcoholByMl())
        u""" 1 合飲んだ焼酎の飲酒の量を 合 で """
        self.assertEqual(45, self.d1.getAlcoholByMl())
        u""" 100 ml 飲んだウイスキーの飲酒の量を ml で """
        self.assertEqual(45, self.d2.getAlcoholByMl())
    
    def testAlcoholRestrictionSetting(self):
        u""" Alcoholrestriction クラスの設定 """
        self.assertEqual(90, self.alcoholRestriction.getLimitOfAlcoholByMl())

    def testCmpQuantity(self):
        # 単位が同じ場合
        self.assertEqual(0,
            cmp(Liquor.Quantity(10, self.unitGou),
                Liquor.Quantity(10, self.unitGou)))
        self.assertEqual(-1,
            cmp(Liquor.Quantity(9, self.unitGou),
                Liquor.Quantity(10, self.unitGou)))
        self.assertEqual(1,
            cmp(Liquor.Quantity(11, self.unitGou),
                Liquor.Quantity(10, self.unitGou)))
                
        self.assertEqual(True,
            Liquor.Quantity(10, self.unitGou) ==
            Liquor.Quantity(10, self.unitGou))
        self.assertEqual(True,
            Liquor.Quantity(10, self.unitGou) <=
            Liquor.Quantity(10, self.unitGou))
        self.assertEqual(True,
            Liquor.Quantity(10, self.unitGou) >=
            Liquor.Quantity(10, self.unitGou))
        self.assertEqual(False,
            Liquor.Quantity(10, self.unitGou) <
            Liquor.Quantity(10, self.unitGou))
        self.assertEqual(False,
            Liquor.Quantity(10, self.unitGou) >
            Liquor.Quantity(10, self.unitGou))
            
        self.assertEqual(True,
            Liquor.Quantity(9, self.unitGou) <
            Liquor.Quantity(10, self.unitGou))
        self.assertEqual(True,
            Liquor.Quantity(11, self.unitGou) >
            Liquor.Quantity(10, self.unitGou))

        # 単位が異なる場合
        self.assertEqual(0,
            cmp(Liquor.Quantity(10, self.unitGou),
                Liquor.Quantity(1800, self.unitMl)))
        self.assertEqual(-1,
            cmp(Liquor.Quantity(9, self.unitGou),
                Liquor.Quantity(1800, self.unitMl)))
        self.assertEqual(1,
            cmp(Liquor.Quantity(11, self.unitGou),
                Liquor.Quantity(1800, self.unitMl)))

        self.assertEqual(True,
            Liquor.Quantity(10, self.unitGou) ==
            Liquor.Quantity(1800, self.unitMl))
        self.assertEqual(True,
            Liquor.Quantity(10, self.unitGou) <=
            Liquor.Quantity(1800, self.unitMl))
        self.assertEqual(True,
            Liquor.Quantity(10, self.unitGou) >=
            Liquor.Quantity(1800, self.unitMl))
        self.assertEqual(True,
            Liquor.Quantity(9, self.unitGou) <
            Liquor.Quantity(1800, self.unitMl))
        self.assertEqual(True,
            Liquor.Quantity(11, self.unitGou) >
            Liquor.Quantity(1800, self.unitMl))
            
        self.assertEqual(False,
            Liquor.Quantity(10, self.unitGou) <
            Liquor.Quantity(1800, self.unitMl))
        self.assertEqual(False,
            Liquor.Quantity(10, self.unitGou) >
            Liquor.Quantity(1800, self.unitMl))

    def testAlcoholRestrictionPermit(self):
        u""" 飲酒リストのテスト """
        self.alcoholRestriction.setDrinkList(self.dl)
        self.assertEqual(False,
            self.alcoholRestriction.permit())
        
        self.alcoholRestriction.setDrinkList(self.dl2)
        self.assertEqual(True,
            self.alcoholRestriction.permit())

        # 2 合 ぎりぎり
        self.alcoholRestriction.setDrinkList(self.dl3)
        self.assertEqual(True,
            self.alcoholRestriction.permit())

        self.dl3.add(self.d1)
        self.alcoholRestriction.setDrinkList(self.dl3)
        self.assertEqual(False,
            self.alcoholRestriction.permit())
            
    def testAllcoholRestrictionPermit2(self):
        u""" Liquor モジュールにおいて定義してある簡便な方法をテスト """
        # 飲酒は「日本酒で2合まで」という制約決める
        alcoholRestriction = Liquor.AlcoholRestriction(Liquor.DRINK_SAKE(2))
        # 日本酒を 1 合飲んだ
        drinkList = Liquor.DrinkList().add(Liquor.DRINK_SAKE(1))
        # 飲酒制約を設定する
        alcoholRestriction.setDrinkList(drinkList)
        # 飲酒の総量は許容範囲か?
        self.assertEqual(True, alcoholRestriction.permit())
        # 焼酎を 0.7 合飲んだ
        drinkList.add(Liquor.DRINK_SAKE(0.7))
        self.assertEqual(True, alcoholRestriction.permit())
        # ウイスキーを 30 ml 飲んだ
        drinkList.add(Liquor.DRINK_WHISKEY(30))
        self.assertEqual(False, alcoholRestriction.permit())
        # 焼酎を 0.01 合飲んだ
        drinkList.add(Liquor.DRINK_SAKE(0.01))
        self.assertEqual(False, alcoholRestriction.permit())
        
            
if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG,
        format='%(asctime)s %(levelname)s ' + \
               '"%(pathname)s", line %(lineno)d, %(message)s')
    logging.getLogger().setLevel(logging.DEBUG)
    unittest.main()