きっかけ
令和もついに4年目。
2019年、新元号が令和になると発表を受け、システム屋たちは新元号対応に追われていましたね。
最初から全てを西暦でやってしまえば楽なのは楽なんですよ。
でも一部の人たちはどうしても和暦が使いたいそうですね。
思うのは勝手なんですけど、それに対応させろって言われるプログラマー側は(きっと)大変なんですよ。
言うのは簡単なんですよ。でもやる側はコードを書いておしまいってわけにはいかないんです。
私たちは平成何年のとか令和何年のとか言われるのに対して、ログの類は全てファイル名が西暦年、月、日で書いてあるからわざわざ頭の中で西暦と和暦の変換をしないといけないんです。
そのたびに私は「和暦じゃなくて西暦で言えよ!」って思うんですよ。でも言う側はそんなの知ったこっちゃないと和暦で物事を話してくるんです。
そんなことが続いたある日、私はこう思いました。
こっちが和暦に対応すればいいんじゃね?
でも当時の私には和暦に対応させる技術力なんてありませんでした。
だから西暦・和暦対照表を作成しました。でもすっげー手間でした。
そんな矢先、天皇陛下の生前退位の話が出てきました。
はっきり言って「マジか…」って思いましたね。
時期を同じくして、私はときめきアイドルのリザルトランキングサービスのAPIの設計作業をやっていました。
「とりあえず設定はJSONで…」って考えた時、私はこんなことを思いつきました。
和暦の一覧をJSONにまとめてそれを読み込めばいいじゃん
そして生まれたのが今回のコードです。
コード
#pragma once
#include <time.h>
#include <string>
struct japanese_tm : public tm {
	japanese_tm() = default;
	japanese_tm(const tm& t) : tm(t), tm_japanese_year_k(), tm_japanese_year_a(), tm_japanese_year() {}
	std::string tm_japanese_year_k; // kanji notation of the Japanese era
	std::string tm_japanese_year_a; // alphabet notation of the Japanese era
	int tm_japanese_year; // Japanese calendar year
};
#pragma once
#include "../date.hpp"
#include <string>
struct japanese_calendar_border_table {
	japanese_calendar_border_table() = default;
	japanese_calendar_border_table(const std::string& jcalendar, const std::string& alphabet, const Date& border)
		: m_jcalendar(jcalendar), m_alphabet(alphabet), m_border(border) {}
	std::string m_jcalendar;
	std::string m_alphabet;
	Date m_border;
};
#pragma once
#include "../json.hpp"
#include "border_table.hpp"
#include <vector>
class japanese_calendar_border_list : public std::vector<japanese_calendar_border_table> {
private:
	using MyClass = std::vector<japanese_calendar_border_table>;
public:
	japanese_calendar_border_list(std::istream& is)
		: japanese_calendar_border_list(json::parse(is)) {}
private:
	japanese_calendar_border_list(const nlohmann::json& json) : MyClass() {
		if (!json["borders"].is_array()) throw std::runtime_error("json format is invalid");
		for (const auto& i : json["borders"]) {
			if (!i.contains("begin")) throw std::runtime_error("require begin in object");
			const auto BeginData = i["begin"];
			if (!BeginData.is_array()) throw std::runtime_error("begin is invalid");
			if (BeginData.size() != 3) throw std::runtime_error("begin is invalid");
			MyClass::push_back(
				japanese_calendar_border_table(
					i["jcalendar"].get<std::string>(),
					i["jalphabet"].get<std::string>(),
					Date(BeginData.at(0).get<int>(), BeginData.at(1).get<int>(), BeginData.at(2).get<int>())
				)
			);
		}
	}
};
#pragma once
#include <time.h>
struct Date {
	Date() = default;
	Date(const int y, const int m, const int d)
		: m_year(y), m_month(m), m_day(d) {}
	Date(const tm& time) : Date(time.tm_year + 1900, time.tm_mon + 1, time.tm_mday) {}
	bool operator == (const tm& timedata) const noexcept {
		return timedata.tm_year + 1900 == this->m_year && timedata.tm_mon + 1 == this->m_month && timedata.tm_mday == this->m_day;
	}
	bool operator != (const Date& timedata) {
		return this->operator==(timedata);
	}
	bool operator < (const tm& timedata) const noexcept {
		return this->m_year < (timedata.tm_year + 1900)
			|| (this->m_year == (timedata.tm_year + 1900) && (this->m_month < timedata.tm_mon + 1))
			|| (this->m_year == (timedata.tm_year + 1900) && (this->m_month == timedata.tm_mon + 1) && this->m_day < timedata.tm_mday);
	}
	bool operator <= (const tm& timedata) const noexcept { return this->operator<(timedata) || this->operator==(timedata); }
	bool operator > (const tm& timedata) const noexcept { return !this->operator<=(timedata); }
	bool operator >= (const tm& timedata) const noexcept { return !this->operator<(timedata); }
	bool operator == (const Date& timedata) const noexcept {
		return timedata.m_year + 1900 == this->m_year && timedata.m_month + 1 == this->m_month && timedata.m_day == this->m_day;
	}
	bool operator != (const Date& timedata) const noexcept {
		return !this->operator==(timedata);
	}
	bool operator < (const Date& timedata) const noexcept {
		return this->m_year < (timedata.m_year + 1900)
			|| (this->m_year == (timedata.m_year + 1900) && (this->m_month < timedata.m_month + 1))
			|| (this->m_year == (timedata.m_year + 1900) && (this->m_month == timedata.m_month + 1) && this->m_day < timedata.m_day);
	}
	bool operator <= (const Date& timedata) const noexcept { return this->operator<(timedata) || this->operator==(timedata); }
	bool operator > (const Date& timedata) const noexcept { return !this->operator<=(timedata); }
	bool operator >= (const Date& timedata) const noexcept { return !this->operator<(timedata); }
	int m_year;
	int m_month;
	int m_day;
};
#pragma once
#include <chrono>
#include <stdexcept>
namespace time_wrapper {
#if defined NO_TIME_S
	namespace {
		inline bool localtime_s(struct tm* result, const time_t* timeval) {
			return !localtime_r(timeval, result);
		}
		inline bool gmtime_s(struct tm* result, const time_t* timeval) {
			return !gmtime_r(timeval, result);
		}
	}
#endif
	tm localtime(const time_t& time) {
		tm timedata{};
		if (localtime_s(&timedata, &time))
			throw std::runtime_error("error in localtime_s function.");
		return timedata;
	}
	tm gmtime(const time_t& time) {
		tm timedata{};
		if (gmtime_s(&timedata, &time))
			throw std::runtime_error("error in gmtime_s function.");
		return timedata;
	}
	tm get_current_clock_l() {
		return localtime(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
	}
	tm get_current_clock_g() {
		return gmtime(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
	}
}
#pragma once
enum class TimeMode : int { UseLocalTime = 0, UseGMTime = 1 };
#pragma once
#include <nlohmann/json.hpp>
namespace json {
	inline nlohmann::json parse(std::istream& is) {
		if (!is) throw std::runtime_error("istream is invalid");
		return nlohmann::json::parse(std::string((std::istreambuf_iterator<char>(is)), std::istreambuf_iterator<char>()));
	}
};
#pragma once
#include "timemode.hpp"
#include "date.hpp"
#include "time_wrapper.hpp"
#include "japanese_calendar/tm.hpp"
#include "japanese_calendar/border_list.hpp"
class japanese_calendar {
private:
	japanese_calendar_border_list list;
public:
	japanese_calendar(std::istream& border) : list(border) {}
	japanese_tm get(const tm& time) const noexcept {
		auto it = std::find_if(this->list.cbegin(), this->list.cend(), [&time](const japanese_calendar_border_table& j) { return j.m_border > time; });
		if (it != this->list.begin()) it--;
		japanese_tm ret(time);
		ret.tm_japanese_year = 1901 + time.tm_year - it->m_border.m_year;
		ret.tm_japanese_year_k = it->m_jcalendar;
		ret.tm_japanese_year_a = it->m_alphabet;
		return ret;
	}
	japanese_tm get(const time_t& time, const TimeMode& time_mode = TimeMode::UseLocalTime) const {
		switch (time_mode) {
			case TimeMode::UseLocalTime:
				return this->get(time_wrapper::localtime(time));
			case TimeMode::UseGMTime:
				return this->get(time_wrapper::gmtime(time));
			default:
				throw std::runtime_error("invalid time_mode");
		}
	}
	japanese_tm get(const TimeMode& time_mode = TimeMode::UseLocalTime) const {
		switch (time_mode) {
			case TimeMode::UseLocalTime:
				return this->get(time_wrapper::get_current_clock_l());
			case TimeMode::UseGMTime:
				return this->get(time_wrapper::get_current_clock_g());
			default:
				throw std::runtime_error("invalid time_mode");
		}
	}
};
今回はヘッダーが結構バラバラに分かれてるため載せるコード数がちょっと多めになっちゃいましたが、本体は一番最後のjapanese_calendar.hppです。
ちなみにその他のそれぞれのヘッダーの役割は次の通りです。
- 
date.hpp
日付の大小比較。現在の和暦を選択するのに使用します。 - 
time_wrapper.hpp
localtime_s、gmtime_s、localtime_r、gmtime_rと環境ごとにバラバラなtmへの変換関数をラップし、localtime、gmtimeに統一する - 
timemode.hpp
localtimeかgmtimeかどっちを使うかを選択する場面における選択肢用のenumを提供 - 
json.hpp
nlohmann/jsonをstd::istreamで使えるように無理矢理変換するためのラッパー
※これ呼ぶ場合はusing namespace nlohmann;使用禁止 - 
japanese_calendar/border_list.hpp
元号一覧表を読み込み、バッファしておくためのクラス - 
japanese_calendar/border_table.hpp
japanese_border_listのバッファー用の型 - 
japanese_calendar/tm.hpp
構造体tmに和暦情報を載せたjapanese_tm構造体を提供 
コード見れば分かると思いますが、使い方そのものは至ってシンプルです。
#include "japanese_calendar.hpp"
#include <iostream>
#include <fstream>
int main() {
    try {
        std::ifstream ifs("./japanese_calendar.json");
        const japanese_calendar cal(ifs);
        const japanese_tm timeinfo = cal.get();
        
        std::cout << "今日は" 
            << timeinfo.tm_japanese_year_k << timeinfo.tm_japanese_year << "年" 
            << (timeinfo.tm_mon + 1) << "月"
            << (timeinfo.tm_mday) << "日です。" << std::endl;
    }
    catch (const std::exception& er) {
        std::cout << er.what() << std::endl;
    }
    return 0;
}
実行結果
ちなみにこのクラスで使うJSONファイルはこんな感じです。
{
  "borders": [
    {
      "jcalendar": "明治",
      "jalphabet": "M",
      "begin": [ 1868, 1, 25 ]
    },
    {
      "jcalendar": "大正",
      "jalphabet": "T",
      "begin": [ 1912, 7, 30 ]
    },
    {
      "jcalendar": "昭和",
      "jalphabet": "S",
      "begin": [ 1926, 12, 25 ]
    },
    {
      "jcalendar": "平成",
      "jalphabet": "H",
      "begin": [ 1989, 1, 8 ]
    },
    {
      "jcalendar": "令和",
      "jalphabet": "R",
      "begin": [ 2019, 5, 1 ]
    }
  ]
}
jcalendar:和暦の漢字表記
jalphabet:和暦のアルファベット1文字表記
begin   :当該元号の開始日(年、月、日の順に区切って格納)
余談
実はこれとは別に、西暦年月日を和暦年月日に変換するサービスを提供しています。
親の誕生日を和暦で書かないといけないけど分からないとかいう時にどうぞ。
APIリファレンスはこちら。
サイト上で実際の日付変換もできます。
