В настоящее время я сдаю старые экзамены из своего курса, чтобы подготовиться к предстоящему экзамену. Меня интересует блок кода, который я укажу после того, как представлю различные файлы с кодом.
//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
петля с try
—catch
блок внутри. Это твердое решение?
Я столкнулся с проблемой, что хотел создать свою дату внутри try
—catch
, но тогда его объем не позволяет использовать после catch
. Моим решением было создать d1
в основной функции, но мне кажется, что это немного шатко? Есть ли лучший способ решить эту проблему?
Большая часть кода в файлах заголовка и имплантации дана и / или должна быть очень похожей. Не стесняйтесь дать мне любой другой аванс, если хотите, и большое спасибо, если вы потратили на это свое время!
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;
}
Тестирование добавления даты может быть легко включено в эту структуру.
Если цель состоит в том, чтобы вычислить дату через 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
Спасибо за ответ!
— exer240