1212#include < span>
1313#include < array>
1414#include < chrono>
15+ #include < cstdint>
1516#include < optional>
1617#include < string>
1718#include < string_view>
2526 * These types are shared deliberately — a tag means the same thing whether it hangs
2627 * off a manga, an anime or an image container, so a consumer learns them once. Two
2728 * conventions run through the file and are worth reading before the individual
28- * fields: an absent value (an empty string, a nullopt, @ref aniparse::unknown_time )
29+ * fields: an absent value (an empty string, a @c nullopt @ref aniparse::ModelDate )
2930 * always means "the source did not state it", never "the item does not have it"; and
3031 * anything named @c ref is an opaque parser-owned handle, to be stored and passed
3132 * back, never parsed.
@@ -305,6 +306,38 @@ inline constexpr std::string_view aired_status_ongoing = "ongoing";
305306// / Canonical @ref AiredStatus::name for @ref aniparse::DefaultAiredStatuses::Announced.
306307inline constexpr std::string_view aired_status_announced = " announced" ;
307308
309+ /* *
310+ * @brief How precisely a source dated something — the granularity of a @ref aniparse::ModelDate.
311+ *
312+ * A source may give a full calendar day, or only a coarser bucket: a month, a quarter
313+ * (an anime "season" — Winter/Spring/Summer/Fall map onto Q1..Q4), or a bare year. The
314+ * date's @ref aniparse::ModelDate::time is pinned to the FIRST instant of that bucket (a year →
315+ * Jan 1, Q3 → Jul 1), so ordering still works; this field says how much of it is real,
316+ * so a consumer renders "2025" / "Q1 2025" / "March 2025" / the full date instead of a
317+ * fabricated January 1.
318+ */
319+ enum class DatePrecision : std::uint8_t {
320+ Day, // /< A full calendar day is known (the default). Render the whole date.
321+ Month, // /< Only the month is known — render e.g. "March 2025".
322+ Quarter, // /< Only the quarter / anime season — @ref aniparse::ModelDate::time is its first day.
323+ Year, // /< Only the year is known — render e.g. "2025".
324+ };
325+
326+ /* *
327+ * @brief A moment a source attaches to an item, carrying how precisely it is known.
328+ *
329+ * Wrapped rather than a bare time_point so the precision cannot drift from the value.
330+ * A date field is @c std::optional<ModelDate>: @c nullopt means the source gave no date
331+ * at all — an absent date is absent, not a sentinel epoch — and a present value is
332+ * always a real moment plus its @ref aniparse::DatePrecision.
333+ */
334+ struct ModelDate {
335+ // / The instant, pinned to the first moment of the precision bucket (@ref aniparse::DatePrecision).
336+ std::chrono::system_clock::time_point time;
337+ // / How much of @ref aniparse::ModelDate::time the source actually stated.
338+ DatePrecision precision = DatePrecision::Day;
339+ };
340+
308341/* *
309342 * @brief Where an item stands in its publication/airing life: released, ongoing,
310343 * announced, or something only that source names.
@@ -322,11 +355,11 @@ struct AiredStatus {
322355 // / status; that reads as @ref aniparse::DefaultAiredStatuses::Other, so an item whose
323356 // / status is unknown is not silently reported as released.
324357 std::string name;
325- // / The moment the source attaches to that state, when it gives one. Equal to
326- // / @ref aniparse::unknown_time (the default) when the source states only the state and no
327- // / date — the common case, so treat a date here as a bonus and never as a
328- // / reliable ordering key. Not to be confused with the item's own release_time.
329- std::chrono::system_clock::time_point time;
358+ // / The moment the source attaches to that state, when it gives one. @c nullopt (the
359+ // / default) when the source states only the state and no date — the common case, so
360+ // / treat a date here as a bonus and never as a reliable ordering key. Not to be
361+ // / confused with the item's own release_time.
362+ std::optional<ModelDate> time;
330363
331364 /* *
332365 * @brief Classify @ref name into the well-known set.
@@ -337,14 +370,6 @@ struct AiredStatus {
337370 DefaultAiredStatuses to_enum () const ;
338371};
339372
340- /* *
341- * The value every time_point in the model carries when the source states no time:
342- * the system_clock epoch, not a real timestamp. Every unset date field (an item's
343- * release_time, a chapter's update_time, @ref aniparse::AiredStatus::time) compares equal to
344- * this, so "unknown" is testable rather than merely early. A consumer must check for
345- * it before formatting a date, or it will show 1970.
346- */
347- inline constexpr std::chrono::system_clock::time_point unknown_time{std::chrono::system_clock::duration{0 }};
348373
349374/* *
350375 * Structure representing rating (score) of the item (release, manga, etc.)
@@ -526,9 +551,9 @@ struct Comment {
526551 RelatedUser author;
527552 // / The body, with any links the source marked up preserved as attributes.
528553 AttributedText text;
529- // / When it was posted; @ref aniparse::unknown_time when the source does not date it
554+ // / When it was posted; @c nullopt when the source does not date it
530555 // / (some report only a relative "2 days ago" the parser will not guess from).
531- std::chrono::system_clock::time_point time = unknown_time ;
556+ std::optional<ModelDate> time;
532557 // / Net score / likes, if the source exposes one. Source-defined and possibly
533558 // / negative where downvotes exist. nullopt = the source has no voting on
534559 // / comments, or does not report it — never "zero votes".
0 commit comments