Добавить 10000 дней к дате

В настоящее время я сдаю старые экзамены из своего курса, чтобы подготовиться к предстоящему экзамену. Меня интересует блок кода, который я укажу после того, как представлю различные файлы с кодом.

//upggift1.cc он же главный

#include "date.h"
#include <iostream>

using namespace std;

int main()
{
    Date d1;
    bool create_date_again = true;
    int temp_date[3];
    char garbage;
    cout << "Enter a date: ";
    cin >> temp_date[0];
    cin >> garbage;
    cin >> temp_date[1];
    cin >> garbage;
    cin >> temp_date[2];

    while (create_date_again)
    {
        try
        {
            Date date{temp_date[0], temp_date[1], temp_date[2]};
            d1 = date;

            create_date_again = false;
        }
        catch (const std::exception &e)
        {
            cout << "invalid date, enter another date: ";
            cin >> temp_date[0];
            cin >> garbage;
            cin >> temp_date[1];
            cin >> garbage;
            cin >> temp_date[2];
            
            create_date_again = true;
        }
    }

    for (int i{}; i < 10000; ++i)
    {
        d1.tomorrow();
    }

    cout << "10000 days later: ";
    d1.print(cout);

    return 0;
}

//date.h

#ifndef DATE_H
#define DATE_H

#include <ostream>

class Date
{
public:
    Date();
    Date(int const y, int const m, int const d);

    bool is_leap_year() const;

    int days_in_month() const;

    void print(std::ostream& os) const;

    void tomorrow();

private:
    int year;
    int month;
    int day;

    void increment();
};
#endif

//date.cc

#include "date.h"
#include <iostream>
#include <string>
#include <array>

using namespace std;

Date::Date()
    : year{}, month{}, day{}
{
}

Date::Date(int const y, int const m, int const d)
    : year{y}, month{m}, day{d}
{
    if (month < 1 || month > 12)
    {
        throw std::domain_error{"Month " + std::to_string(month) + " doesn't exist!"};
    }

    if (day < 1 || day > days_in_month())
    {
        throw std::domain_error{"Day " + std::to_string(day) + " invalid"};
    }
}

int Date::days_in_month() const
{
    static constexpr const std::array<int, 13> days{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    if (month < 1 || month > 12)
        return 0;

    if (month == 2 && is_leap_year())
        return 29;

    return days.at(month);
}

bool Date::is_leap_year() const
{
    if (year % 400 == 0)
        return true;
    if (year % 100 == 0)
        return false;

    return year % 4 == 0;
}

void Date::print(std::ostream &os) const
{
    os << year;
    if (month < 10)
    {
        os << "-0" << month;
    }
    else
    {
        os << "-" << month;
    }
    if (day < 10)
    {
        os << "-0" << day;
    }
    else
    {
        os << "-" << day;
    }
    os << endl;
}

void Date::tomorrow()
{
    day++;
    if (is_leap_year())
    {
        if (day > 29 && month == 2)
        {
            day = 1;
            month++;
            return;
        }
        if (month != 2)
        {
            increment();
        }
    }
    else
    {
        increment();
    }
}
void Date::increment()
{
    if (day > days_in_month())
    {
        day = 1;
        month++;
    }
    if (month > 12)
    {
        month = 1;
        year++;
    }
}

Это та часть, в которой я особенно не уверен (uppgift1.cc файл):

while (create_date_again)
    {
        try
        {
            Date date{temp_date[0], temp_date[1], temp_date[2]};
            d1 = date;

            create_date_again = false;
        }
        catch (const std::exception &e)
        {
            cout << "invalid date, enter another date: ";
            cin >> temp_date[0];
            cin >> garbage;
            cin >> temp_date[1];
            cin >> garbage;
            cin >> temp_date[2];
            
            create_date_again = true;
        }
    }

Вот как я должен выполнять программу и что сейчас произойдет.

./a.out 
Enter a date:2013-13-02
Invalid date, enter another date:2013-12-42
Invalid date, enter another date:1900-02-29
Invalid date, enter another date:1987-04-13
10000 days later: 2014-08-29
    

Мое решение продолжать создавать новую дату, пока она не будет в правильном формате, — это while петля с trycatch блок внутри. Это твердое решение?

Я столкнулся с проблемой, что хотел создать свою дату внутри trycatch, но тогда его объем не позволяет использовать после catch. Моим решением было создать d1 в основной функции, но мне кажется, что это немного шатко? Есть ли лучший способ решить эту проблему?

Большая часть кода в файлах заголовка и имплантации дана и / или должна быть очень похожей. Не стесняйтесь дать мне любой другой аванс, если хотите, и большое спасибо, если вы потратили на это свое время!

3 ответа
3

upggift1.cc

Пожалуйста, не делайте этого:

using namespace std;

У нас есть пространства имен не зря, и отказываться от их преимуществ — плохая привычка.

Date d1;
bool create_date_again = true;
int temp_date[3];
char garbage;

Это похоже на то, что написал (довольно старый) программист на C. Предпочитайте объявлять переменные там, где они могут быть инициализированы, а не все в начале блока, как это. Это выглядит как temp_date должно быть три отдельные переменные, поскольку мы никогда не используем их как массив.

cout << "Enter a date: ";

Мы должны быть более конкретными с сообщением, поскольку пользователи ожидают, что смогут вводить дату в обычном формате их собственной локали, например, 19 марта 2021 года, но нам здесь требуется гораздо более конкретный формат.

cin >> temp_date[0];
cin >> garbage;
cin >> temp_date[1];
cin >> garbage;
cin >> temp_date[2];

Нет проверки, все ли входные данные были успешно преобразованы. Таким образом, мы не знаем, были ли присвоены все четыре переменные.

    catch (const std::exception &e)

Почему мы обнаруживаем такой широкий спектр исключений? В частности, это поймает std::bad_alloc и дают очень плохой ответ на это.

    catch (const std::exception &e)
    {
        cout << "invalid date, enter another date: ";
        cin >> temp_date[0];
        cin >> garbage;
        cin >> temp_date[1];
        cin >> garbage;
        cin >> temp_date[2];
        
        create_date_again = true;
    }

Здесь у нас есть повторение кода, который мы уже написали. Нам не нужно снова писать этот ввод. И все еще нет проверки на нечисловой ввод или конец файла (что дает бесконечный цикл, когда я запускаю программу без ввода).

И почему мы назначаем create_date_again то же самое значение, которое, как мы уже знаем, оно содержит?


date.h

#include <ostream>

Лучше включить <iosfwd> когда нам нужны только объявления.

Date();

Есть ли конструктор по умолчанию — хорошая идея? Это требует большого количества логики во всех функциях для правильной обработки созданной по умолчанию (недействительной) даты. Лучше использовать std::optional где коду нужна концепция «Не свидание».

Date(int const y, int const m, int const d);

Объявление формальных параметров const здесь нет никакого эффекта, и поэтому бесполезный беспорядок.

void print(std::ostream& os) const;

Обычно мы предоставляем operator<<() вместо этого для более удобного использования.

void tomorrow();

Плохое название для функции, изменяющей экземпляр.


date.cc

if (month < 1 || month > 12)
{
    throw std::domain_error{"Month " + std::to_string(month) + " doesn't exist!"};

Я думаю std::invalid_argument там более логичный выбор. std::domain_error больше подходит для математических операций (например, обращения матрицы).

Когда мы ловим исключение в main(), мы должны извлекать и отображать сообщение, чтобы помочь пользователю. В противном случае мы просто напрасно потратили свои усилия на создание значимого сообщения.

bool Date::is_leap_year() const
{
    if (year % 400 == 0)
        return true;
    if (year % 100 == 0)
        return false;

    return year % 4 == 0;
}

Вероятно, это наименее эффективный способ написать это условие. Сначала проверьте наиболее вероятный случай:

bool Date::is_leap_year() const
{
    if (year % 4)
        return false;
    if (year % 100)
        return true;
    return year % 400 == 0;
}
if (month < 10)
{
    os << "-0" << month;
}
else
{
    os << "-" << month;
}

Вместо этого if/else прочее, просто используйте форматирование потока, чтобы использовать 2 цифры с нулями:

os << '-' << std::setw(2) << std::setfill('0') << month;
os << endl;

Избегать std::endl — просто используйте простой n и пусть вызывающий сам решает, когда нужно очистить поток.

day++;

Предпочитайте предварительное приращение, когда значение не используется. Хотя это не имеет значения с int, постоянство облегчает читателю.

tomorrow() кажется слишком сложным, особенно потому, что у нас уже есть days_in_month() чью логику мы, кажется, повторяем.


Вот что-нибудь попроще и надежнее:

#include "date.h"
#include <iostream>
#include <limits>

static Date get_date(std::istream& is = std::cin, std::ostream& os = std::cout)
{
    for (;;) {
        os << "Enter a date (y-m-d): ";
        char separator;
        int year, month, day;
        is >> year >> separator >> month >> separator >> day;
        if (!is) {
            // discard the erroneous input line
            is.clear();
            is.ignore(std::numeric_limits<std::streamsize>::max(), 'n');
            if (is.eof()) {
                throw std::ios_base::failure("Stream read failure");
            }
            os << "Invalid format.";
            continue;
        }
        try {
            return {year, month, day};
        } catch (std::invalid_argument& e) {
            os << e.what() << "  ";
            // and go round again
        }
    }
}

int main()
{
    try {
        auto date = get_date();
        for (auto i = 0;  i < 10000;  ++i) {
            date.advance();
        }
        std::cout << "10000 days later: " << date << 'n';
    } catch (std::runtime_error& e) {
        std::cerr << e.what();
    }
}
#ifndef DATE_H
#define DATE_H

#include <iosfwd>

class Date
{
public:
    Date(int y, int m, int d);

    bool is_leap_year() const;
    int days_in_month() const;

    void advance();

    friend std::ostream& operator<<(std::ostream& os, const Date& date);

private:
    int year;
    int month;
    int day;
};
#endif
#include "date.h"
#include <array>
#include <iomanip>
#include <iostream>
#include <string>

Date::Date(int const y, int const m, int const d)
    : year{y}, month{m}, day{d}
{
    if (month < 1 || month > 12) {
        throw std::invalid_argument{"Month " + std::to_string(month) + " doesn't exist!"};
    }

    if (day < 1 || day > days_in_month()) {
        throw std::invalid_argument{"Day " + std::to_string(day) + " doesn't exist!"};
    }
}

int Date::days_in_month() const
{
    constexpr std::array days{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    if (month == 2 && is_leap_year())
        return 29;

    return days.at(month);
}

bool Date::is_leap_year() const
{
    return year % 4 == 0 && year % 100 || year % 400 == 0;
}

std::ostream& operator<<(std::ostream& os, const Date& date)
{
    return
        os << date.year
           << '-' << std::setw(2) << std::setfill('0') << date.month
           << '-' << std::setw(2) << std::setfill('0') << date.day;
}

void Date::advance()
{
    ++day;
    if (day <= days_in_month()) {
        return;
    }
    ++month;
    day = 1;
    if (month <= 12) {
        return;
    }
    ++year;
    month = 1;
}

В этой версии я выделил get_date() функция. Мы можем проверить это независимо. Обычно для тестирования я бы использовал одну из распространенных фреймворков модульного тестирования, но для этого мы можем сколотить что-нибудь довольно простое:

#include <sstream>

// return error count - 0 on success
int test_fail(const std::string& input)
{
    std::istringstream is{input};
    std::ostringstream os{};
    try {
        Date d = get_date(is, os);
        // shouldn't get here
        std::cerr << "FAIL: "" << input
                  << "" produced " << d
                  << " instead of failure.n";
        return 1;
    } catch (std::ios_base::failure&) {
        // as expected
        return 0;
    }
}

int test_succeed(const std::string& input)
{
    std::istringstream is{input};
    std::ostringstream os{};
    try {
        get_date(is, os);
        // as expected
        return 0;
    } catch (std::ios_base::failure&) {
        // shouldn't get here
        std::cerr << "FAIL: "" << input
                  << "" raised exception.n";
        return 1;
    }
}

int main()
{
    int errors =
        + test_fail("")
        + test_fail("not a date")
        + test_succeed("not a daten2000-01-01")
        + test_fail("0-0-0")
        + test_fail("2000-01-0")
        + test_succeed("2000-01-01")
        + test_succeed("2000 01 01")
        + test_succeed("2000+01+01")
        + test_succeed("2000O01O01") // do we really want to allow this??
        + test_succeed("2000-01-31")
        + test_fail("2000-01-32")
        + test_succeed("2000-02-29")
        + test_fail("2000-02-30")
        + test_succeed("2004-02-29")
        + test_fail("2004-02-30")
        + test_succeed("2001-02-28")
        + test_fail("2001-02-29")
        + test_succeed("2100-02-28")
        + test_fail("2100-02-29")
        ;
    if (errors == 1) {
        std::cerr << "There was one test failure.n";
    } else if (errors) {
        std::cerr << "There were " << errors << " test failures.n";
    }
    return errors > 0;
}

Тестирование добавления даты может быть легко включено в эту структуру.

  • Спасибо за ответ!

    — exer240

Если цель состоит в том, чтобы вычислить дату через 10000 дней, я настоятельно рекомендую использовать std::chrono удобства. Если вы это сделаете, код будет длиной всего в несколько строк:

#include <iostream>
#include <iomanip>
#include <ctime>
#include <chrono>

int main()
{
    const std::chrono::time_point<std::chrono::system_clock> now =
        std::chrono::system_clock::now();
    using days = std::chrono::duration<unsigned, std::ratio_multiply<std::ratio<24>, std::chrono::hours::period>>;
    days period(10000);
    const std::time_t later = std::chrono::system_clock::to_time_t(now + period);
    std::cout << "10,000 days from now the date will be "
              << std::put_time(std::localtime(&later), "%x.n") << std::flush;
}

    Использование исключений при проверке ввода данных пользователем — не обычная практика. Более простой подход был бы для Date класс, чтобы предоставить статическую функцию-член, чтобы проверить, можно ли ее построить из заданных аргументов:

    class Date
    {
    public:
        static is_valid(int y, int m, int d);
        Date(int y, int m, int d);
    

    Тогда вместо throw/catch, мы можем читать ввод до тех пор, пока Date::is_valid() возвращает истину, а затем продолжайте.

    Если мы хотим улучшить сообщения об ошибках, мы могли бы разделить функцию:

        static is_valid_month(int y, int m, int d);
        static is_valid_day(int y, int m, int d);
    

    Я займусь этим 🙂

    — exer240

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

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