Python一些可能用的到的函数系列6 自定义时间对象

说明

python的时间模块实在有点让人晕。主要的有两个,一个是time模块,一个是datetime模块。这两个模块的有部分功能还是重叠的,实在不想忍了- - !
我打算重新造一个轮子,只要效率还可以,然后直观好用就行了。
实现的功能有以下几块:

  1. 字符串和时间戳的互转
  2. 实现月偏移:月偏移从计算上是不精确的,尽量保证号相同。(例如4.1到5.1,偏移一个月是30天;但5.1到6.1偏移一个月是31天)。不过通常来说,如果要按做偏移尽量还是选1号吧,毕竟每个月都有(月末可就不一定哪天了)。
  3. 实现秒偏移(周、天、时、分):精确偏移,这个是要换算成时间戳,那就随便整了。
  4. 其他(没想好,需要的时候再加吧)

时间

时间要同时服务好机器和人类。对于机器而言,就是时间戳;对人而言就是各种形式的字符,这里规定标准的字符格式是yyyy-mm-dd (hh:mm:ss),时分秒可以省略。

要能够做一个沟通人类和计算的时间,就要先按照人类的历法来整理,也就是所谓的万年历。

首先分平年和闰年,闰年的话2月多一天,有2月29日。
其次就是大小月,这个小时候掰着手指头记过(一月大,二月小,三月大…)
万年历, 真的按一万年来规划的话,一共有10000个年份(嗯,废话);如果按照年月来规划的话,一共有12万个年月(嗯,又一句废话)。

我的想法

虽然时间推算的方法是固定的,但是如果要大批量计算(例如一百万行),那么每次都重新根据规则推算是挺浪费计算资源,也挺慢的。
所以,先设定一个基准年份,和可能用到的最大推算年份(例如基准年之后的一万年),把对应的参数一次性计算好放在内存里,然后就近比较偏移就好了。

先看结果

先说结论 百万级别的数据转化要30秒左右(单进程):

  1. 整体转换效率比pd.to_datetime要快一倍(当然to_datetime更复杂,带了很多方法)
  2. 万年历和百年历效率查了100倍(看来keep大的内存还是会吃力的)
  3. 随机抽查,结果是对的。pd.to_datetime直接转为数值是格林尼治时间,差了8小时。
  4. 如果希望进一步提升速度的化,可以缩小到类似二十年之类的。(但是有可能要改代码,算了,真要快我就用并行算就好了)
     ...: df_test = pd.concat(df_sub*10, ignore_index=True) 
     ...: df_test.shape
     ...: t1 = time.time()
     ...: df_test['dt_str'] = df_test['xx时间'].apply(str)
     ...: t2 = time.time()
     ...: print('>>> 字符串化时间', t2- t1)
     ...: df_test['dt_ts1'] = df_test['dt_str'].apply(bb2.dt2seconds)
     ...: t3 = time.time()
     ...: print('>>> 百年历时间', t3 -  t2)
     ...: 
     ...: df_test['dt_ts2'] = df_test['dt_str'].apply(bb1.dt2seconds)
     ...: t4 = time.time()
     ...: print('>>>万年历时间', t4- t3)
     ...: df_test['dt_ts_pd'] = df_test['dt_str'].apply(pd.to_datetime)
     ...: t5 = time.time()
     ...: print('>>> pd datetime 时间', t5- t4)
     ...: df_test['dt_ts2'] = df_test['dt_str'].apply(bb3.dt2seconds)
     ...: t6 = time.time()
     ...: print('>>>六十年历时间', t6- t5)
     ...: 
     ...: 
>>> 字符串化时间 0.05991196632385254
>>> 百年历时间 0.31140995025634766
>>>万年历时间 34.879563093185425
>>> pd datetime 时间 0.6795589923858643
>>>六十年历时间 0.2773611545562744

代码

下面是代码,里面有了一些注释,我再捋一捋。@staticmethod可以认为为了代码干净,把相关的函数绑在一个类下面。此外还有@classmethod和@property, classmethod和staticemethod类似,但还可以访问类通用属性。property可以用来做一些类属性的限制(例如密码属性不能直接访问,要通过property函数)

class ATimer():
    def __init__(self, base_year=1970,  next_years=10000,time_zone=8):
        self.base_year = base_year
        self.time_zone = time_zone
        self.year_bias_days_dict, self.year_bias_seconds_dict ,self.year_list = ATimer.gen_years_base(base_year, next_years=next_years, time_zone=time_zone)
        self.a_dict_days, self.b_dict_days, self.a_dict_seconds, self.b_dict_seconds = ATimer.ab_calendar()
        self.year_mon_days_dict, self.year_mon_seconds_dict = ATimer.get_year_mon_base(
                                                                                    self.year_bias_days_dict, 
                                                                                    self.a_dict_days, 
                                                                                    self.b_dict_days,
                                                                                    time_zone=time_zone)

    # 判断是否为闰年
    @staticmethod
    def is_ryear(some_year):
        some_year = int(some_year)
        return True if some_year % 400 == 0 or (some_year % 4 == 0 and some_year % 100 != 0) else False
    # 判断年.月的天数
    @staticmethod
    def yymon_days(yy, mon):
        list31 = [1, 3, 5, 7, 8, 10, 12]
        list30 = [4, 6, 9, 11]
        if mon in list31:
            return 31
        elif mon in list30:
            return 30
        else:
            if ATimer.is_ryear(yy):
                return 29
            else:
                return 28
    
    # 计算闰年的天数
    @staticmethod
    def ryear_days(yy):
        if ATimer.is_ryear(yy):
            return 366
        else:
            return 365

    # 字符日期和数值的映射
    @staticmethod
    def mon_next(someday, next = 0):
        someday = str(someday)
        if len(someday) == 1:
            someday = '0'+someday
        elif len(someday) ==2:
            pass
        else:
            someday = someday[:2]
        mon_list  = [str(x) for x in range(1,13)]
        mon_list1 = [x if len(x) == 2 else '0'+x for x in mon_list]
        try:
            current_idx = mon_list1.index(someday)
        except:
            current_idx = 0
        
        target_idx = current_idx + next  
        if target_idx <0:
            target_idx = 11
        elif target_idx >11:
            target_idx = 0
        else:
            pass
        return mon_list1[target_idx]
    # 获取列表的下一个值
    @staticmethod
    def get_list_next(current_value, some_list, next=0):
        some_list = list(some_list)
        try:
            current_idx = some_list.index(current_value)
        except:
            current_idx = None 
        if current_idx is not None :
            if (current_idx + next >=len(some_list)) or (current_idx + next <0):
                next_value = None
            else:
                next_value = some_list[current_idx + next]
        else:
            next_value = None 
        return next_value

    # 字符日期和数值的映射,对应月末是28, 29, 30, 31四种类型的
    @staticmethod
    def day_next(someday, next=0, mtype='A'):
        # 31 大约
        if mtype =='A':
            max_day = 32
        # 30 小月
        elif mtype =='B':
            max_day = 31
        # 29 二月闰月
        elif mtype =='C':
            max_day = 30
        # 28 二月平月
        else:
            max_day = 29
        someday = str(someday)
        if len(someday) == 1:
            someday = '0'+someday
        elif len(someday) == 2:
            pass
        else:
            someday = someday[:2]
        day_list = [str(x) for x in range(1, max_day)]
        day_list1 = [x if len(x) == 2 else '0'+x for x in day_list]
        try:
            current_idx = day_list1.index(someday)
        except:
            current_idx = 0

        target_idx = current_idx + next
        if target_idx < 0:
            target_idx = max_day-2
        elif target_idx > max_day-2:
            target_idx = 0
        else:
            pass
        return day_list1[target_idx]


    # 计算每个年份的基准偏移(1月1号0时0分0秒和选定的base_year的偏移),直接枚举一万年的偏移方便之后计算
    @staticmethod
    def gen_years_base(base_year, next_years = 10000, time_zone = 8):
        s_ = pd.Series(range(base_year, base_year+next_years + 1))
        s1_ = s_.apply(ATimer.ryear_days).shift().fillna(0).cumsum()
        # year_bias_days_dict 
        year_bias_days_dict = cl.OrderedDict(zip(s_.values, s1_.values))
        # year_bias_seconds_dict
        year_bias_seconds_dict = cl.OrderedDict(zip(s_.values, (s1_.values)*3600*24 - 3600*time_zone))
        # 年列表
        year_list = list(year_bias_seconds_dict.keys())

        return year_bias_days_dict, year_bias_seconds_dict, year_list

    # 平闰年枚举(平年日历与闰年日历),返回两个OrderedDict
    # a平年, b闰年
    @staticmethod
    def ab_calendar():
        list31 = [1, 3, 5, 7, 8, 10, 12]
        list30 = [4, 6, 9, 11]
        a_dict = cl.OrderedDict()
        a_dict_seconds = cl.OrderedDict()
        acnt = 0
        for m in list(range(1,13)):
            if m in list31:
                # 1~31
                mday_list = list(range(1, 32))
            elif m in list30:
                # 1~30
                mday_list = list(range(1,31))
            else:
                # 1~28
                mday_list = list(range(1,29))
            for md in mday_list:
                strm = str(m) 
                strmd = str(md)
                strm1 = strm if len(strm) ==2 else '0'+strm
                strmd1 = strmd if len(strmd) == 2 else '0' + strmd
                a_dict[strm1 + '-'+ strmd1] = acnt
                a_dict_seconds[strm1 + '-' + strmd1] = acnt*3600*24
                acnt +=1

        b_dict = cl.OrderedDict()
        b_dict_seconds = cl.OrderedDict()
        acnt = 0
        for m in list(range(1, 13)):
            if m in list31:
                # 1~31
                mday_list = list(range(1, 32))
            elif m in list30:
                # 1~30
                mday_list = list(range(1, 31))
            else:
                # 1~29
                mday_list = list(range(1, 30))
            for md in mday_list:
                strm = str(m)
                strmd = str(md)
                strm1 = strm if len(strm) == 2 else '0'+strm
                strmd1 = strmd if len(strmd) == 2 else '0' + strmd
                b_dict[strm1 + '-'+ strmd1] = acnt
                b_dict_seconds[strm1 + '-' + strmd1] = acnt*3600*24
                acnt += 1
        return a_dict, b_dict, a_dict_seconds, b_dict_seconds
        

    # 正则判定 可转时间字符:接受的标准格式是 yyyy-mm-dd (HH:MM:SS)或者是yyyy/mm/dd (HH:MM:SS)
    @staticmethod
    def is_retime_str(some_str):
        # 正则表达式
        pass

    # 年月基准:使用年偏移,根据平闰年获取年日历,取每个月的1号0时0分0秒作为月偏移
    # year_bias_days_dict 是按年的天偏移,a_dict是平年日历按天偏移, b_dict是闰年日历按天偏移
    @staticmethod
    def get_year_mon_base(year_bias_days_dict, a_dict, b_dict, time_zone=8):
        year_mon_days_dict = {}
        year_mon_seconds_dict = {}
        for yyyy in year_bias_days_dict:
            yyyy1 = str(yyyy) + '-'
            if ATimer.is_ryear(yyyy):
                current_year_dict = b_dict
            else:
                current_year_dict = a_dict
            # 获取12个月的1号
            mon_list = [x for x in current_year_dict.keys() if x.endswith('01')]
            for mon in mon_list:
                mon1 = mon[:2]
                year_mon_days_dict[yyyy1+mon1] = year_bias_days_dict[yyyy] + current_year_dict[mon]
                year_mon_seconds_dict[yyyy1+mon1] = year_mon_days_dict[yyyy1+mon1] * 86400 - 3600*time_zone
        return year_mon_days_dict, year_mon_seconds_dict

    # 将时分秒转为秒
    @staticmethod
    def HMS2seconds(hms_string):
        hh, mm, ss = hms_string.replace(':',':').split(':')
        if hh.startswith('0'):
            hh1 = int(hh[1:])
        else:
            hh1 = int(hh)
        if mm.startswith('0'):
            mm1 = int(mm[1:])
        else:
            mm1 = int(mm)
        if ss.startswith('0'):
            ss1 = int(ss[1:])
        else:
            ss1 = int(ss)
        seconds = hh1 * 3600 + mm1 * 60 + ss1
        return seconds

    # 将小于一天86400的秒转为小时
    @staticmethod
    def HMS2string(seconds):
        hh = str(seconds // 3600)
        hh_left_seconds = seconds % 3600
        mm = str(hh_left_seconds // 60 )
        mm_left_seconds = str(int(mm) % 60)
        if len(hh) ==1:
            hh1 = '0' + hh 
        else:
            hh1 = hh
        if len(mm) == 1:
            mm1 = '0' + mm
        else:
            mm1 = mm
        if len(mm_left_seconds) == 1:
            mm_left_seconds1 = '0' + mm_left_seconds
        else:
            mm_left_seconds1 = mm_left_seconds
        return ':'.join([hh1, mm1, mm_left_seconds1])

    # 将日期转为数值(秒)- 默认样式为yyyy-mm-dd (hh:mm:ss),如果原始数据格式不对可以在之前加格式转换
    # year_mon_seconds_dict (使用秒级的)
    @staticmethod
    def DT2seconds(dt_string, mon_year_base):
        string_list = dt_string.strip().split(' ')
        if len(string_list) == 2:
            dt_part, hms_part = string_list[0], string_list[1]
            hms_seconds = ATimer.HMS2seconds(hms_part)
        else:
            dt_part, hms_part = string_list[0], 0
            hms_seconds = 0
        # 校验 - 有效的日数加上后不应该超过下个月的时间戳
        rpos = dt_part.rfind('-')
        yymon = dt_part[:rpos]
        yymon_next = ATimer.get_list_next(yymon, mon_year_base.keys(), next=1)
        dd = dt_part[rpos+1:]
        if dd.startswith('0'):
            dd1 = int(dd[1:]) -1 
        else:
            dd1 = int(dd) -1
        day_seconds = dd1*86400
        

        day_seconds1 = day_seconds + hms_seconds
        # 获取当前月份(1号0时0分0秒)的时间戳(和下个月的)
        yymon_ts = mon_year_base[yymon]
        yymon_ts1 = mon_year_base[yymon_next]
        if yymon_ts + day_seconds1 < yymon_ts1:
            res = yymon_ts + day_seconds1
        else:
            res = None 

        return res
    # 将时间戳转为字符 - 如果需要别的格式另外再加
    @staticmethod
    def DT2str(dt_seconds, mon_year_base , str_mode = None):
        dt_seconds = float(dt_seconds)
        base_keys = list(mon_year_base.keys())
        base_values = np.array(list(mon_year_base.values())).astype(float)
        pos = int((dt_seconds >= base_values).sum() -1 )
        val = base_keys[pos]
        # 获得的val是当月的基准
        # 剩下的就用秒数相加了
        left_seconds = int(dt_seconds - mon_year_base[val])
        the_day = (left_seconds // 86400 + 1)
        if len(str(the_day)) ==1 :
            the_day1 = '0' + str(the_day)
        else:
            the_day1 = str(the_day)
        hms_seconds = left_seconds % 86400
        # 如果是None就自行推断,如果是1就显示带hms, 否则显示不带hms的
        if str_mode is None:
            if hms_seconds == 0:
                res = val + '-' + the_day1
            else:
                res = val + '-' + the_day1 + ' ' + ATimer.HMS2string(hms_seconds)
        elif str_mode == 1:
            res = val + '-' + the_day1 + ' ' + ATimer.HMS2string(hms_seconds)
        else:
            res = res = val + '-' + the_day1

        return res
    

    # 实例方法 - 将字符串转为时间戳 - 时区
    def dt2seconds(self, dt_string):
        return ATimer.DT2seconds(dt_string, self.year_mon_seconds_dict)

    # 实例方法 - 将时间戳转为字符串 + 时区
    def dt2str(self, dt_seconds, str_mode=None):
        return ATimer.DT2str(dt_seconds, self.year_mon_seconds_dict, str_mode = str_mode)

一些函数的说明,基本上也就是从上到下的操作

序号函数名作用
1is_ryear判断某年是否是闰年,后面是其他函数的基础方法
2yymon_days判断年月的天数(因为平闰年,年月的天数不同)
3ryear_days如果是平年返回355天,如果是闰年返回366天
4mon_next返回下一个月,嗯,这个函数好像没什么用
5get_list_next获取列表的下一个值,这个好用,在推算下一个月的时候用了
6day_next根据不同类型的月份(28,29,30,31) 获取下一天,也没啥用
7gen_years_base根据设定的基准年份,和年份周期,设定对应的时区,然后生成周期中每一年1月1日0时0分对应的日偏移和秒偏移
8ab_calendar生成平闰年的日历,按顺序从1-1列到12-31,其中每一天对应年初始值的偏移天数和秒数
9is_retime_str正则判定一个字符串是否是可解析日期,暂时空着
10get_year_mon_base这个厉害了,根据前面获得的周期年日偏移字典,结合平闰年,生成一个年月的日偏移和秒偏移字典,类似 2010-01, 2010-02 … 这样的,后序进行映射就靠这个了
13HMS2seconds把符合 HH:MM:SS 格式的时间转为秒
14HMS2string把秒转回HH:MM:SS格式,请自行控制秒值不大于86400,不然我也不知道会算出什么,哈哈
15DT2seconds把时间日期字符串转为秒(时间戳)
16DT2str把时间戳转回字符日期
17dt2seconds实例方法,使用初始化的值直接转换,将日期转为时间戳
18dt2str实例方法,使用初始化的值直接转换,将时间戳转为日期

使用方法:

1. 初始化 | 使用1970年开始60年以内(2030年)的日历
bb3 = ATimer( next_years = 60)
2. 转换 | 把dt_str字段转为时间戳
df_test['dt_ts2'] = df_test['dt_str'].apply(bb3.dt2seconds)
3. 逆转换 | 把时间戳转回来
df_test['dt_ts2_inv'] = df_test['dt_ts2'].apply(bb3.dt2str)

--- 成功
In [327]: df_test[['dt_str','dt_ts2','dt_ts2_inv']].head()
Out[327]: 
                dt_str        dt_ts2           dt_ts2_inv
0  2019-12-14 15:21:00  1.576308e+09  2019-12-14 15:21:21
1  2019-12-14 15:21:00  1.576308e+09  2019-12-14 15:21:21
2  2020-05-26 23:02:00  1.590505e+09  2020-05-26 23:02:02
3  2020-05-26 23:02:00  1.590505e+09  2020-05-26 23:02:02
4  2020-05-30 13:16:00  1.590816e+09  2020-05-30 13:16:16

后续

之后大约会把增补的内容列在这里,但是代码就不再贴了,容易乱。
另外关于是否要重复造轮子的问题,之前一股潮流是不要重复造轮子,个人的感觉是有点过了。是否重复造轮子要看具体使用者的需求,专业运动员的装备和普通人随便玩玩的肯定是不同的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值