《数据结构与算法(Python语言描述)》第二章练习——Date

创建一个Date数据结构,并进行测试

1. 实现部分

书中对Date数据结构提出了几个方法,要求实现这几种方法,下面对这些方法进行分析:

  • Date(int year,int month,int day) # 构造表示year/month/day的对象
    显而易见,我们要对输入的参数进行验证,如果不符合参数为整数的要求,就抛出一个错误;另外需要注意,对于参数是否符合日期合法性也需要验证;
    例如:润年2月有29天,而平年2月只有28天。
  • difference(Date d1, Date d2) # 求出d1和d2的日期差
    有两种方法:
    1. 按位置相加减,year - year/month - month/day - day,再根据正负情况分别进行转换成天数。
    2. 将两者先转换为天数,算出从小的日期大的日期经过了多少天。
  • plus(Date d,int n) # 计算出日期d之后n天的日期
    同样两种方法:

    1. day + n,然后进行判断:
      如果增加之后,day没有超过month的最大天数,则月份不变。
      如果超过了month的最大天数,则月份进行更新。
    2. Date d转化成天数,进行days + n,然后再将days转换成日期形式。

    注意:这里要考虑一个问题,如果n超过year的最大天数(356 or 366),我们的程序依然要支持这种操作。

  • num_date(int year,int n) # 计算year年第n天的日期
    这里考虑同上,可以从year1月1日日期的方式计算,也可以通过将n转换成天数来计算。
  • adjust(Date d, int n) # 将日期d调整n天(n为带符号的整数)
    n可以是正的,日期向后推进;
    n也可以是负的,日期需要向前推导;
    同样需要考虑是否超出month,还要考虑是否超过year的days

综上我采取了将日期转换为天数进行计算的方式。

首先我们创建一个针对Date的错误类型:

class DateValueError(ValueError):
    pass

我们创建一个Date的错误类型,这个类型从ValueError内置错误类型继承,只用来显示我们指定的错误信息。

现在初始化这个Date数据结构:

class Date:
    def __init__(self, year, month, day):
        if not isinstance(year, int) or not\
        (
        isinstance(month, int) and \
        isinstance(day, int)
        ): # 判断类型是否符合要求
            raise DateValueError("只支持int类型的参数")

        MonthDays = Date.isR(year)
        # isR方法根据year是润年还是平年,返回year的月份表
        # 例如:[31,28,31,30,31.......]
        if day <= 0 or day > MonthDays[month - 1]:
            raise DateValueError("日期不合法")
        else:
            self._year = year
            self._month = month
            self._day = day
            self._days = Date.Days(year, month, day)# 将日期转化为当前年的天数
    def __str__(self):
        return "%s/%s/%s" % (self._year, self._month, self._day)

__init__的if语句可能稍微有点难读,拆开分析一下:

  • not isinstance(year, int):如果year不是int类型,则这个语句返回True
  • not (isinstance(month,int) and isinstance(day,int)):如果括号内的任何一个类型不为int,则返回True

整个if语句保证了三个参数,任何一个不满足类型要求,就会抛出DateValueError错误。

现在我们需要一个将日期转换为天数的方法(Days方法)和返回月份列表的方法(isR方法):

@classmethod
def isR(cls, year):
    MonthsDay = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    if (year % 100 and not year % 4) or \
    (not year % 100 and not year % 400): # 如果year是润年
        MonthsDay[1] = 29
    return MonthsDay

@classmethod
def Days(cls, year, month, day):
    MonthsDay = Date.isR(year) 
    # isR方法根据year是润年还是平年,返回year的月份表
    # 例如:[31,28,31,30,31.......]
    month -= 1 # 月份在列表中的下标
    if month > 0:
        days = sum(MonthsDay[:month]) + day
    else: # 如果月份month为1
        days = day
    return days

这些方法都比较简单,由于我采用天数计算的方法,还需要一个能将天数转换成日期的方法,这也是我实现这个数据结构的核心:

def __date(self, year, days):# days是相对于year来说的总天数
    # year是起始年,我们一下的操作都以year为基础进行
    yearsday = sum(Date.isR(year)) # 获取起始年总天数
    if days > yearsday:
        while True:
            year += 1 # 更新 年
            yd = sum(Date.isR(year))
            if yearsday + yd >= days:
                days -= yearsday # 更新要处理的天数
                break
            yearsday += yd
    MonthsDay = Date.isR(year) # 更新后的year月份表
    month = 1 # 月份
    while MonthsDay[month - 1] < days:
        days -= MonthsDay[month - 1]
        month += 1 # 如果当前月份天数小于更新过的days, 将月份+1
    return (year, month, days)

这个方法基本思想是这样的:

  • days不超过初始年year的总天数,我们使用while MonthsDay[month - 1]求出days属于这一年的哪个月之后,将剩下的days作为这个日期的day
  • days超过初始年year的总天数时,执行if days > yearsday语句,(请注意,当days == yearsday时,仍没有超出初始年,这时候日期应为xxxx/12/31),这条语句下的循环就一个作用,算出基于初始年的days真正的year是多少,如果前面年累加后的天数 + 下一年的天数 > days,那么days - 前面年累加后的天数,就得到了真正的year是多少,然后正常的执行程序就可以了。

现在来实现两个简单的方法:

def plus(self, n):
    return "%s/%s/%s" % self.__date(self._year, self._days + n)

def num_date(self, year, n):
    return "%s/%s/%s" % self.__date(year, n)

非常简单,不必特别说明,有点麻烦的是下面的两个方法,如果你有简化的方法不妨试一试:

 70     def difference(self, other):
 71         if self._year == other._year: # 如果d1,d2年份相同
 72             return abs(self._days - other._days)
 73 
 74         if self._year < other._year: # 如果self为小的一方
 75             BigYearDays = other._days # 大年分经过的天数
 76             SmallYear = self._year # 小年份是多少
 77             SmallYearDays = self._days # 小年份经过的天数
 78             BigYear = other._year # 大年份是多少
 79         else:
 80             BigYearDays = self._days
 81             SmallYearDays = other._days
 82             SmallYear = other._year
 83             BigYear = self._year
 84         Sday = sum(Date.isR(SmallYear)) # 小的年的总天数
 85         days = Sday - SmallYearDays # 小年分的总天数 - 小年分经过的天数 = 小年分剩余的部分
 86         while True:
 87             SmallYear += 1
 88             if SmallYear == BigYear:
 89                 break
 90             days += sum(Date.isR(SmallYear)) # 更新相差天数
 91         days += BigYearDays
 92         return days

difference方法之所以有点复杂,是因为需要区分小年份和大年份,所以做了一些重复的工作,如果感兴趣的话可以进行简化。

100     def adjust(self, n):
101         days = self._days + n
102         year = self._year # 获得当前年为多少
103         if days <= 0: # 当前年的天数小于等于n时候
104             while days <= 0:
105                 year -= 1 # 年向前推
106                 yearday = sum(Date.isR(year)) # 获取当前年总天数
107                 days += yearday
108         date = self.__date(year, days)
109         self._year, self._month, self._day = date
110         self._days = Date.Days(self._year, self._month, self._day)# 更新`days`

对于adjust方法,当n为负数,并且超过了self._days,就需要将year向过去推导,会执行if days <= 0语句,剩下的就是更新self._year/self._month/self._dayself._days 的信息。上面的各个方法还应该加一个验证参数部分,因为实现很简单,写的话有点罗嗦,这里进行省略。

2. 测试部分

这里使用了Pythonunittest库进行测试,使用非常简单,功能也比较强大,下面是实现:

import unittest

132 class TestDate(unittest.TestCase):
133     def test_value_no_int(self):# 测试参数类型错误
134         self.assertRaisesRegex(DateValueError, "value type is not `int`", Da    te.__init__, Date,"2018", "7", "26")
135     
136     def setUp(self): # setUp是一个特殊方法,会在每个测试方法运行前被调用
137         self.d1 = Date(2017, 12, 31)
138         self.d2  = Date(2019, 1, 1)
139 
140     def test_value_wrongful(self): # 测试日期不合法时的错误
141         self.assertRaisesRegex(DateValueError, "日期不合法", Date.__init__,     Date, 2018, 2, 29)
142 
143     def test_Date_str(self):
144         # 测试__str__方法
146         self.assertEqual(str(self.d1), "2017/12/31")
147         self.assertEqual(str(self.d2), "2019/1/1")
148 
149     def test_Date_difference(self):# 测试difference方法
150         # d1 = Date(2017, 12, 31)
151         # d2  = Date(2019, 1, 1)
152         self.assertEqual(self.d1.difference(self.d2), 366)
153         self.assertEqual(self.d2.difference(self.d1), 366)
154 
155     def test_Date_plus(self):# 测试plus方法
156         # d1 = Date(2017, 12, 31)
157         # d2  = Date(2019, 1, 1)
158         self.assertEqual(self.d1.plus(1), "2018/1/1")
159         self.assertEqual(self.d2.plus(58), "2019/2/28")
160         self.assertEqual(self.d2.plus(365), "2020/1/1")
161 
162     def test_Date_num_date(self):# 测试num_date方法
163         self.assertEqual(self.d2.num_date(2016, 60), "2016/2/29")
164         self.assertEqual(self.d1.num_date(2018, 60), "2018/3/1")
165 
166     def test_Date_adjust(self): # 测试adjust方法
167         d1 = Date(2016, 12, 31)
168         d1.adjust(-366)
169         self.assertEqual(str(d1), "2015/12/31")
170         d1.adjust(366)
171         self.assertEqual(str(d1), "2016/12/31")# 测试日期更改后,是否还能改动回来
172 
173 
174         self.d1.adjust(-365) 
175         self.assertEqual(str(self.d1), "2016/12/31")
176         self.d1.adjust(365)
177         self.assertEqual(str(self.d1), "2017/12/31")
178         
179         self.d2.adjust(730)
180         self.assertEqual(str(self.d2), "2020/12/31")
181         self.d2.adjust(-365)
182         self.assertEqual(str(self.d2), "2020/1/1")
183 
184 if __name__ == "__main__":
185     unittest.main()

以上测试基本涵盖了能想到的所有边界:

  • year为润年时,xxxx/1/1调用plus(n = 59)后为xxxx/2/29year为平年调用plus(n = 59)后,日期为xxxx/3/1
  • year为润年时,例如2016/12/31调用adjust(n = -356),日期为2016/1/1year为平年,例如2018/12/31,则日期为2017/12/31
  • 省略(主要是adjust参数横跨平、润年时的种种测试)

可以说这是一个通用的测试,你可以复制下来进行自己的测试,当然,格式可能不对,需要自己调整,如果需要的话可以给我个邮箱。
在命令行调用python3 -m unittest -v Date.py就能看到测试的结果。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值