创建一个Date数据结构,并进行测试
1. 实现部分
书中对Date数据结构提出了几个方法,要求实现这几种方法,下面对这些方法进行分析:
Date(int year,int month,int day) # 构造表示year/month/day的对象
:
显而易见,我们要对输入的参数进行验证,如果不符合参数为整数的要求,就抛出一个错误;另外需要注意,对于参数是否符合日期合法性也需要验证;
例如:润年2月有29天,而平年2月只有28天。difference(Date d1, Date d2) # 求出d1和d2的日期差
:
有两种方法:
- 按位置相加减,
year - year/month - month/day - day
,再根据正负情况分别进行转换成天数。 - 将两者先转换为天数,算出从小的日期到大的日期经过了多少天。
- 按位置相加减,
plus(Date d,int n) # 计算出日期d之后n天的日期
:
同样两种方法:- 用
day + n
,然后进行判断:
如果增加之后,day
没有超过month
的最大天数,则月份不变。
如果超过了month
的最大天数,则月份进行更新。 - 将
Date d
转化成天数,进行days + n
,然后再将days
转换成日期形式。
注意:这里要考虑一个问题,如果
n超过year的最大天数(356 or 366)
,我们的程序依然要支持这种操作。- 用
num_date(int year,int n) # 计算year年第n天的日期
:
这里考虑同上,可以从year
的1月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._day
和self._days
的信息。上面的各个方法还应该加一个验证参数部分,因为实现很简单,写的话有点罗嗦,这里进行省略。
2. 测试部分
这里使用了Python的unittest
库进行测试,使用非常简单,功能也比较强大,下面是实现:
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/29
;year
为平年调用plus(n = 59)
后,日期为xxxx/3/1
。 - 当
year
为润年时,例如2016/12/31
调用adjust(n = -356)
,日期为2016/1/1
;year
为平年,例如2018/12/31
,则日期为2017/12/31
。 - 省略(主要是
adjust
参数横跨平、润年时的种种测试)
可以说这是一个通用的测试,你可以复制下来进行自己的测试,当然,格式可能不对,需要自己调整,如果需要的话可以给我个邮箱。
在命令行调用python3 -m unittest -v Date.py
就能看到测试的结果。