Код Python для поиска разницы между двумя датами

В последнее время я работал над переносом своих сценариев PowerShell 7 на Python 3.9.5, повторно реализую ту же логику на другом языке программирования, сохраняя то, что заставляет его работать, и удаляя ненужные части, улучшая код, и это один из сценариев. это было повторно реализовано.

Код прост, и я знаю, что это реализовывалось бесчисленное количество раз, но он выполняет свою работу без использования встроенной datetime модуль, и он принимает две строки, представляющие даты в девяти форматах дат, и возвращает разницу между двумя датами:

'yyyy-MM-dd'
'yyyy/MM/dd'
'MM/dd/yyyy'
'MMM dd, yyyy'
'dd MMM, yyyy'
'MMMM dd, yyyy'
'dd MMMM, yyyy'
'yyyy, MMM dd'
'yyyy, MMMM dd'

И он полностью рабочий.

Итак, вот код:

import re
import sys

Months = (
    {'Month':'January', 'Days':31},
    {'Month':'February', 'Days':28},
    {'Month':'March', 'Days':31},
    {'Month':'April', 'Days':30},
    {'Month':'May', 'Days':31},
    {'Month':'June', 'Days':30},
    {'Month':'July', 'Days':31},
    {'Month':'August', 'Days':31},
    {'Month':'September', 'Days':30},
    {'Month':'October', 'Days':31},
    {'Month':'November', 'Days':30},
    {'Month':'December', 'Days':31}
)

Culture = (
    {'Format':'yyyy-MM-dd', 'Regex':'^d{4}-0?([2-9]|1[0-2]?)-(0?(3[01]|[12][0-9]|[1-9]))$'},
    {'Format':'yyyy/MM/dd', 'Regex':'^d{4}/0?([2-9]|1[0-2]?)/(0?(3[01]|[12][0-9]|[1-9]))$'},
    {'Format':'MM/dd/yyyy', 'Regex':'^0?([2-9]|1[0-2]?)/(0?(3[01]|[12][0-9]|[1-9]))/d{4}$'},
    {'Format':'MMM dd, yyyy', 'Regex':'^[A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9])), d{4}$'},
    {'Format':'dd MMM, yyyy', 'Regex':'^(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3}, d{4}$'},
    {'Format':'MMMM dd, yyyy', 'Regex':'^[A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9])), d{4}$'},
    {'Format':'dd MMMM, yyyy', 'Regex':'^(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3,9}, d{4}$'},
    {'Format':'yyyy, MMM dd', 'Regex':'^d{4}, [A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9]))$'},
    {'Format':'yyyy, MMMM dd', 'Regex':'^d{4}, [A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9]))$'}
)

def splitter(date, x, y):
    date = date.replace(',','').split(x)
    y = list(map(int, y.split(',')))
    year = int(date[y[0]])
    month = date[y[1]]
    if not re.match('^d+$', month):
        for i in Months:
            if re.match(f'{month}',i['Month']): month = Months.index(i) + 1
    month = int(month)
    day = int(date[y[2]])
    return {'Year': year, 'Month': month, 'Day': day}

def parsedate(date):
    for a in range(len(Culture)):
        if re.match('%s' % Culture[a]['Regex'], date): i = a; break
    if i == 0: return splitter(date, '-', '0,1,2')
    elif i == 1: return splitter(date, "https://codereview.stackexchange.com/", '0,1,2')
    elif i == 2: return splitter(date, "https://codereview.stackexchange.com/", '2,0,1')
    elif i == 3: return splitter(date, ' ', '2,0,1')
    elif i == 4: return splitter(date, ' ', '2,1,0')
    elif i == 5: return splitter(date, ' ', '2,0,1')
    elif i == 6: return splitter(date, ' ', '2,1,0')
    elif i == 7: return splitter(date, ' ', '0,1,2')
    elif i == 8: return splitter(date, ' ', '0,1,2')

def totaldays(date):
    ye = date['Year']
    y = ye
    mon = date['Month']
    d = date['Day']
    if mon <= 2: y -= 1
    leaps = y // 4 - y // 100 + y // 400
    m = 0
    for b in range(mon - 1): m += int(Months[b].get('Days'))
    days = (ye - 1) * 365 + m + d + leaps
    return days

def diffdate(start, end):
    date1 = totaldays(parsedate(start))
    date2 = totaldays(parsedate(end))
    ddate = date2 - date1
    print(ddate)

start = sys.argv[1]
end = sys.argv[2]
diffdate(start, end)

Как обычно, мне интересно, как это можно улучшить, может быть несколько областей, которые можно улучшить, но теперь я в основном забочусь о замене переключателя и о том, как получить параметры из командной строки, 8 операторов elif выполняют свою работу, но заставляют код выглядит ужасно, я попытался использовать словарь, но он выдает ошибку date is undefined, и я не хочу импортировать sys только для того, чтобы получить аргументы из командной строки, я не знаю, есть ли автоматическая переменная, такая как PowerShell $Args.

Пожалуйста, помогите мне улучшить мой код, вы можете поделиться всем, что вы думаете о моем коде.

2 ответа
2

Современный Python имеет так много возможностей для создания простых объектов данных (namedtuple, dataclass и превосходный attrs library), что обычно неправильно структурировать ваши внутренние данные с помощью обычных dicts, списков или кортежей. Эти простые объекты данных не только очищают код с точки зрения удобочитаемости (как показано в нескольких местах ниже), но также побуждают вас сосредоточить свое внимание там, где оно должно быть: определение значимых объектов данных для облегчения и поддержки алгоритмических потребностей программы . Организуйте данные правильно, и алгоритмы обычно будут работать естественным образом.

Показательный пример: вам не нужен 8-позиционный переключатель; вам нужны более богатые данные. В
splitter() аргументы принадлежат области данных (Culture экземпляры в коде ниже), а не алгоритм.

Другой пример: не используйте в своем алгоритме данные, требующие синтаксического анализа. Например, если вы считаете, что вам нужен кортеж индексов, таких как (2, 0, 1), не храните его в строке, разделенной запятыми, и не усложняйте свой алгоритмический код логикой распаковки. Просто сохраните индексы напрямую как кортеж целых чисел. Основная проблема при написании программного обеспечения — это уменьшение сложности и управление ею, а самая большая сложность всегда связана с алгоритмами. Если вы инвестируете больше средств в создание «более интеллектуальных» или «более богатых» данных, это поможет вам упростить понимание и поддержку алгоритмических частей кода.

Python имеет встроенный sum() функция.

Когда возможно (что означает почти всегда), выполнять итерацию непосредственно по коллекции (т. Е. Списку, кортежу), а не по индексам. Если вам тоже нужны индексы, используйте enumerate().

Откажитесь от своего стиля попытки втиснуть лишнюю логику в каждую строчку:

if foo > 1233: x = 12; y = 34

Я прошел через эту фазу на короткое время в начале своего пути к Python. Он плохо масштабируется, и его сложнее поддерживать в течение длительного периода в проекте. Опять же, сложность — самый большой враг в разработке программного обеспечения: один из способов немного снизить сложность (и повысить удобочитаемость) — отдать предпочтение более коротким строкам кода над более длинными. В пределах разумного, более высокие / тонкие тексты легче для читателей, чем более короткие / широкие тексты (издательская индустрия понимает это на протяжении десятилетий), поэтому, если вы втисываете больше кода в отдельные строки, вы экономите не на том (т. Е. Экономите строки пока трачу читаемость).

Точно так же оставьте свое отвращение к импорту частей стандартной библиотеки Python. Это нормально — избегать этих вещей во имя обучения. Но это единственная веская причина, о которой я знаю. Так что получите аргументы командной строки из sys.argv
без вины. Это путь!

После внесения упрощений из этих изменений splitter() функция кажется ненужной. Вместо этого я бы предложил другую вспомогательную функцию просто для анализа месяца. И эта логика не требует регулярного выражения: вы можете использовать более простые вещи, например isdigit() а также startswith(). Также Month должен знать его номер. Опять же, используйте более обширные данные, чтобы упростить алгоритм.

Другие улучшения, которые вам стоит рассмотреть:

  • Если вы вложите больше средств в свои регулярные выражения, чтобы они использовали именованные захваты, вы можете отказаться от утомительных деталей в Culture.separator а также
    Culture.indexes. Вместо этого вы можете получить год, месяц и день прямо из re.Match объект, по имени. Еще раз, умные данные, простой код.

  • При неверном вводе ваш код просто взрывается. Подумайте, хотите ли вы инвестировать в лучшую обработку ошибок / отчетность.

  • Даты, как известно, сложны. В вашем коде, вероятно, есть странные крайние ошибки. Вы уверены, что не хотите использовать datetime?


import re
import sys
from collections import namedtuple

# Simple data objects are handy as hell. Use more of them.

Month = namedtuple('Month', 'name number days')
Culture = namedtuple('Culture', 'format separator indexes regex')
Date = namedtuple('Date', 'year month day')

Months = (
    Month('January', 1, 31),
    Month('February', 2, 28),
    Month('March', 3, 31),
    Month('April', 4, 30),
    Month('May', 5, 31),
    Month('June', 6, 30),
    Month('July', 7, 31),
    Month('August', 8, 31),
    Month('September', 9, 30),
    Month('October', 10, 31),
    Month('November', 11, 30),
    Month('December', 12, 31),
)

Cultures = (
    Culture('yyyy-MM-dd',    '-', (0,1,2), '^d{4}-0?([2-9]|1[0-2]?)-(0?(3[01]|[12][0-9]|[1-9]))$'),
    Culture('yyyy/MM/dd',    "https://codereview.stackexchange.com/", (0,1,2), '^d{4}/0?([2-9]|1[0-2]?)/(0?(3[01]|[12][0-9]|[1-9]))$'),
    Culture('MM/dd/yyyy',    "https://codereview.stackexchange.com/", (2,0,1), '^0?([2-9]|1[0-2]?)/(0?(3[01]|[12][0-9]|[1-9]))/d{4}$'),
    Culture('MMM dd, yyyy',  ' ', (2,0,1), '^[A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9])), d{4}$'),
    Culture('dd MMM, yyyy',  ' ', (2,1,0), '^(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3}, d{4}$'),
    Culture('MMMM dd, yyyy', ' ', (2,0,1), '^[A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9])), d{4}$'),
    Culture('dd MMMM, yyyy', ' ', (2,1,0), '^(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3,9}, d{4}$'),
    Culture('yyyy, MMM dd',  ' ', (0,1,2), '^d{4}, [A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9]))$'),
    Culture('yyyy, MMMM dd', ' ', (0,1,2), '^d{4}, [A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9]))$'),
)

# A purely stylistic point: organize modules the way you want to read them.
# Most humans like to read things top to bottom.

def main(args):
    start, end = args
    ddate = diffdate(start, end)
    print(ddate)

def diffdate(start, end):
    d1 = totaldays(parsedate(start))
    d2 = totaldays(parsedate(end))
    return d2 - d1

def totaldays(date):
    y = date.year
    if date.month <= 2:
        y -= 1
    leaps = y // 4 - y // 100 + y // 400
    m = sum(Months[b].days for b in range(date.month - 1))
    days = (date.year - 1) * 365 + m + date.day + leaps
    return days

def parsedate(date):
    for c in Cultures:
        if re.match(c.regex, date):
            parts = date.replace(',', '').split(c.separator)
            return Date(
                int(parts[c.indexes[0]]),
                parsemonth(parts[c.indexes[1]]),
                int(parts[c.indexes[2]]),
            )
    # Raise an error if we get here.

def parsemonth(month):
    if month.isdigit():
        return int(month)
    else:
        for m in Months:
            if m.name.lower().startswith(month.lower()):
                return m.number
    # Raise an error if we get here.

if __name__ == '__main__':
    main(sys.argv[1:])

    Это хороший подход, который вы сделали, если вам нужно пропустить datetime модуль.

    Вы не добавили еще много культур, но это уже другая тема.

    Сначала я отвечу на ваш вопрос, связанный с аргументом командной строки. НЕТ есть нет чистого / хорошего решения для этого.

    Во-вторых, я рассмотрю только основной вопрос, заменив if-else лестница с switch.

    Вы уже проделали за меня основную работу. 🙂

    Decider = [
      ('-', '0,1,2'), ("https://codereview.stackexchange.com/", '0,1,2'), ("https://codereview.stackexchange.com/", '2,0,1'),
      (' ', '2,0,1'), (' ', '2,1,0'), (' ', '2,0,1'),
      (' ', '2,1,0'), (' ', '0,1,2'), (' ', '0,1,2')
    ]
    
    # use like
    splitter(date, *Decider[i])
    

    Надеюсь это поможет.

      Добавить комментарий

      Ваш адрес email не будет опубликован. Обязательные поля помечены *