BitcoinExchange.cpp
1 /* ************************************************************************** */ 2 /* */ 3 /* ::: :::::::: */ 4 /* BitcoinExchange.cpp :+: :+: :+: */ 5 /* +:+ +:+ +:+ */ 6 /* By: gychoi <gychoi@student.42seoul.kr> +#+ +:+ +#+ */ 7 /* +#+#+#+#+#+ +#+ */ 8 /* Created: 2024/01/12 18:13:09 by gychoi #+# #+# */ 9 /* Updated: 2024/01/14 23:06:33 by gychoi ### ########.fr */ 10 /* */ 11 /* ************************************************************************** */ 12 13 #include "BitcoinExchange.hpp" 14 15 std::map<std::time_t, double> BitcoinExchange::sDataMap; 16 17 static bool _isValidDateFormat(std::string const& date); 18 static bool _isValidDouble(std::string const& s); 19 static bool _isNumber(std::string const& s); 20 static bool _isLeapYear(int year); 21 static int _stoi(std::string const& s); 22 static double _stod(std::string const& s); 23 static std::string _trim(std::string const& s); 24 25 /* ************************************************************************** */ 26 /* Constructor & Destructor */ 27 /* ************************************************************************** */ 28 29 BitcoinExchange::BitcoinExchange() 30 { 31 // nothing to do 32 } 33 34 BitcoinExchange::BitcoinExchange 35 (__attribute__((unused)) BitcoinExchange const& other) 36 { 37 // nothing to do 38 } 39 40 BitcoinExchange& BitcoinExchange::operator= 41 (__attribute__((unused)) BitcoinExchange const& other) 42 { 43 return *this; 44 } 45 46 BitcoinExchange::~BitcoinExchange() 47 { 48 // nothig to do 49 } 50 51 /* ************************************************************************** */ 52 /* Public Member Function */ 53 /* ************************************************************************** */ 54 55 void BitcoinExchange::setExchangeMap(char const* path) 56 { 57 std::ifstream dbFile(path); 58 59 if (!dbFile.is_open()) 60 { 61 throw std::runtime_error("could not open DB."); 62 } 63 else 64 { 65 std::string header; 66 67 if (!std::getline(dbFile, header)) 68 { 69 throw std::runtime_error("Unable to read DB header."); 70 } 71 else if (header != "date,exchange_rate") 72 { 73 throw std::runtime_error("Malformed csv header."); 74 } 75 else 76 { 77 // header OK 78 } 79 } 80 81 std::string line; 82 83 while (std::getline(dbFile, line)) 84 { 85 std::istringstream iss(line); 86 std::string date; 87 std::string exchangeRate; 88 std::string remain; 89 90 if (dbFile.fail()) 91 { 92 throw std::runtime_error("Failed to read DB file."); 93 } 94 else if (line.empty()) 95 { 96 continue; 97 } 98 else if (std::getline(iss, date, ',')) 99 { 100 if (!_isValidDateFormat(date)) 101 { 102 throw std::runtime_error(std::string("bad DB input => ") 103 + date); 104 } 105 else if (!(iss >> exchangeRate)) 106 { 107 throw std::runtime_error("Unable to read exchange rate in DB."); 108 } 109 else if (!_isValidDouble(exchangeRate)) 110 { 111 throw std::runtime_error(std::string("bad DB input => ") 112 + exchangeRate); 113 } 114 else if (std::getline(iss, remain)) 115 { 116 throw std::runtime_error(std::string("bad DB input => ") 117 + line); 118 } 119 else 120 { 121 // data format OK 122 } 123 124 double rate = _stod(exchangeRate); 125 126 if (rate < 0) 127 { 128 throw std::runtime_error("not a positive number in DB."); 129 } 130 else if (rate > std::numeric_limits<int>::max()) 131 { 132 throw std::runtime_error("too large a number in DB."); 133 } 134 else 135 { 136 std::tm tm = {}; 137 std::time_t epochTime; 138 139 tm.tm_year = _stoi(date.substr(0, 4)) - 1900; 140 tm.tm_mon = _stoi(date.substr(5, 2)) - 1; 141 tm.tm_mday = _stoi(date.substr(8)); 142 143 if ((epochTime = std::mktime(&tm)) == -1) 144 { 145 throw std::runtime_error("Failed to convert epoch time in DB."); 146 } 147 else 148 { 149 sDataMap.insert(std::make_pair(epochTime, rate)); 150 } 151 } 152 } 153 else 154 { 155 throw std::runtime_error("Unable to read DB data."); 156 } 157 } 158 } 159 160 void BitcoinExchange::execute(char const* path) 161 { 162 std::ifstream inputFile(path); 163 164 if (!inputFile.is_open()) 165 { 166 throw std::runtime_error("could not open file."); 167 } 168 else 169 { 170 std::string header; 171 172 if (!std::getline(inputFile, header)) 173 { 174 throw std::runtime_error("Unable to read input file header."); 175 } 176 else if (header != "date | value") 177 { 178 throw std::runtime_error("Malformed input file header."); 179 } 180 else 181 { 182 // header OK 183 } 184 } 185 186 std::string line; 187 188 while (std::getline(inputFile, line)) 189 { 190 std::istringstream iss(line); 191 std::string date; 192 std::string exchangeRate; 193 std::string remain; 194 195 if (inputFile.fail()) 196 { 197 throw std::runtime_error("Failed to read input file."); 198 } 199 else if (line.empty()) 200 { 201 continue; 202 } 203 else if (std::getline(iss, date, '|')) 204 { 205 date = _trim(date); 206 207 if (!_isValidDateFormat(date)) 208 { 209 std::cout << "Error: bad input => " << date << std::endl; 210 continue; 211 } 212 else if (!(iss >> exchangeRate)) 213 { 214 std::cout << "Error: Unable to read exchange rate." 215 << std::endl; 216 continue; 217 } 218 else if (!_isValidDouble(exchangeRate)) 219 { 220 std::cout << "Error: bad input => " << exchangeRate 221 << std::endl; 222 continue; 223 } 224 else if (std::getline(iss, remain)) 225 { 226 std::cout << "Error: bad input => " << line << std::endl; 227 continue; 228 } 229 else 230 { 231 // data format OK 232 } 233 234 double rate = _stod(exchangeRate); 235 236 if (rate < 0) 237 { 238 std::cout << "Error: not a positive number." << std::endl; 239 continue; 240 } 241 else if (rate > 1000) 242 { 243 std::cout << "Error: too large a number." << std::endl; 244 continue; 245 } 246 else 247 { 248 std::tm tm = {}; 249 std::time_t epochTime; 250 251 tm.tm_year = _stoi(date.substr(0, 4)) - 1900; 252 tm.tm_mon = _stoi(date.substr(5, 2)) - 1; 253 tm.tm_mday = _stoi(date.substr(8)); 254 255 if ((epochTime = std::mktime(&tm)) == -1) 256 { 257 throw std::runtime_error("Failed to convert epoch time."); 258 } 259 else 260 { 261 std::map<std::time_t, double>::const_iterator 262 mit = sDataMap.lower_bound(epochTime); 263 264 if (mit == sDataMap.begin() && epochTime < mit->first) 265 { 266 std::cout << "Error: data not found." << std::endl; 267 continue; 268 } 269 else if (mit == sDataMap.end() || epochTime != mit->first) 270 { 271 --mit; 272 } 273 else 274 { 275 // found exact exchange date 276 } 277 278 std::cout << date << " => " << rate << " = " 279 << rate * mit->second << std::endl; 280 } 281 } 282 } 283 else 284 { 285 throw std::runtime_error("Unable to read input file data."); 286 } 287 } 288 } 289 290 /* ************************************************************************** */ 291 /* Utility Function */ 292 /* ************************************************************************** */ 293 294 static bool _isValidDateFormat(std::string const& date) 295 { 296 int year; 297 int month; 298 int day; 299 300 /* Check date format(YYYY-MM-DD) and parse into int */ 301 if (date.size() != 10 || date[4] != '-' || date[7] != '-') 302 { 303 return false; 304 } 305 else 306 { 307 std::string sYear = date.substr(0, 4); 308 std::string sMonth = date.substr(5, 2); 309 std::string sDay = date.substr(8); 310 311 if (!_isNumber(sYear) || !_isNumber(sMonth) || !_isNumber(sDay)) 312 { 313 return false; 314 } 315 else 316 { 317 year = _stoi(sYear); 318 month = _stoi(sMonth); 319 day = _stoi(sDay); 320 } 321 } 322 323 /* check date has valid range */ 324 if (year < 1900 || year > 9999) 325 { 326 return false; 327 } 328 else if (month < 1 || month > 12) 329 { 330 return false; 331 } 332 else if (day < 1 || day > 31) 333 { 334 return false; 335 } 336 337 /* check date has valid day relate to month */ 338 switch (month) 339 { 340 case 2: 341 { 342 if (day == 29 && !_isLeapYear(year)) 343 { 344 return false; 345 } 346 else if (day > 29) 347 { 348 return false; 349 } 350 else 351 { 352 break; 353 } 354 } 355 case 4: 356 case 6: 357 case 9: 358 case 11: 359 { 360 if (day > 30) 361 { 362 return false; 363 } 364 else 365 { 366 break; 367 } 368 } 369 default: 370 { 371 if (day > 31) 372 { 373 return false; 374 } 375 else 376 { 377 break; 378 } 379 } 380 } 381 382 return true; 383 } 384 385 static bool _isValidDouble(std::string const& s) 386 { 387 char* endptr; 388 389 std::strtod(s.c_str(), &endptr); 390 391 return *endptr == '\0'; 392 } 393 394 static bool _isNumber(std::string const& s) 395 { 396 for (std::string::const_iterator cit = s.begin(); cit != s.end(); ++cit) 397 { 398 if (!std::isdigit(*cit)) 399 { 400 return false; 401 } 402 } 403 404 return true; 405 } 406 407 static bool _isLeapYear(int year) 408 { 409 return (year % 4 == 0 && year % 100 != 0) 410 || (year % 100 == 0 && year % 400 == 0); 411 } 412 413 static int _stoi(std::string const& s) 414 { 415 std::istringstream iss(s); 416 int ret; 417 418 if (!(iss >> ret)) 419 { 420 throw std::runtime_error("input stream error."); 421 } 422 423 return ret; 424 } 425 426 static double _stod(std::string const& s) 427 { 428 std::istringstream iss(s); 429 double ret; 430 431 if (!(iss >> ret)) 432 { 433 throw std::runtime_error("input stream error."); 434 } 435 436 return ret; 437 } 438 439 static std::string _trim(std::string const& s) 440 { 441 std::string ret; 442 443 for (std::string::const_iterator cit = s.begin(); cit != s.end(); ++cit) 444 { 445 if (!std::isspace(*cit)) 446 { 447 ret += *cit; 448 } 449 } 450 451 return ret; 452 } 453