Files
Elternbeirat/Elternbeirat.Web/Features/Events/EventDateFormat.cs
T

214 lines
9.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Elternbeirat.Contracts;
using Elternbeirat.Web.Shared;
namespace Elternbeirat.Web.Features.Events;
/// <summary>
/// Formats the date range of an <see cref="Event"/> for display in German.
/// </summary>
/// <remarks>
/// Shared by the full event list, the home-page events embed and the next-event
/// card, so all render dates the same way: <see cref="Range"/> as the full date,
/// the <c>Sheet…</c> methods and <see cref="Span"/> for the calendar sheet and the
/// time line beside it, <see cref="PastDays"/> and <see cref="PastTime"/> for the list of past events.
/// <para>
/// This is the event counterpart to
/// <see cref="Posts.PostDateFormat"/>, which formats a plain
/// <see cref="DateOnly"/> day instead of a point-in-time range.
/// </para>
/// </remarks>
public static class EventDateFormat
{
/// <summary>
/// Date format for the start, and for a single-day event.
/// </summary>
private const string LongDate = "dddd, d. MMMM yyyy";
/// <summary>
/// Date format for the end of a multi-day event; the weekday is omitted.
/// </summary>
private const string ShortDate = "d. MMMM yyyy";
/// <summary>
/// Date format for the list of past events, which is grouped by year.
/// </summary>
private const string DayMonth = "d. MMMM";
/// <summary>
/// Formats the date range of an event.
/// </summary>
/// <param name="event">
/// The event to format.
/// </param>
/// <returns>
/// The formatted range, depending on <see cref="Event.End"/> and <see cref="Event.HasTime"/>:
/// <list type="bullet">
/// <item>
/// <description>No end: <c>Donnerstag, 8. Oktober 2026, 19:30 Uhr</c></description>
/// </item>
/// <item>
/// <description>Same day: <c>Donnerstag, 8. Oktober 2026, 19:30–21:00 Uhr</c></description>
/// </item>
/// <item>
/// <description>Several days: <c>Freitag, 9. Oktober 2026 – 11. Oktober 2026</c></description>
/// </item>
/// </list>
/// Without a time, the time parts are omitted.
/// </returns>
public static string Range(Event @event)
=> (@event.Start, @event.End, @event.HasTime) switch
{
(var start, null, var hasTime)
=> Stamp(start, LongDate, hasTime),
(var start, { } end, true) when end.Date == start.Date
=> $"{start.ToString(LongDate, Cultures.German)}, {Clock(start)}–{Clock(end)} Uhr",
(var start, { } end, false) when end.Date == start.Date
=> Stamp(start, LongDate, withTime: false),
(var start, { } end, var hasTime)
=> $"{Stamp(start, LongDate, hasTime)} – {Stamp(end, ShortDate, hasTime)}",
};
/// <summary>
/// The short month for the top band of a calendar sheet.
/// </summary>
/// <param name="value">The event's start.</param>
/// <returns>The German short month without its period, e.g. <c>Okt</c> or <c>März</c>.</returns>
/// <remarks>
/// Returned in normal case; the sheet's CSS sets it in capitals. The period of the
/// abbreviation (<c>Okt.</c>) is dropped because on a sheet it reads as a stray dot.
/// </remarks>
/// <seealso cref="CalendarSheet"/>
public static string SheetMonth(DateTime value)
=> value.ToString("MMM", Cultures.German).TrimEnd('.');
/// <summary>
/// The day of the month for the large number on a calendar sheet.
/// </summary>
/// <param name="value">The event's start.</param>
/// <returns>The day without a leading zero, e.g. <c>8</c>.</returns>
public static string SheetDay(DateTime value)
=> value.Day.ToString(Cultures.German);
/// <summary>
/// The short weekday for the bottom of a calendar sheet.
/// </summary>
/// <param name="value">The event's start.</param>
/// <returns>The German short weekday without its period, e.g. <c>Do</c>.</returns>
public static string SheetWeekday(DateTime value)
=> value.ToString("ddd", Cultures.German).TrimEnd('.');
/// <summary>
/// The time line beside a calendar sheet: what the sheet itself cannot show.
/// </summary>
/// <param name="event">The event to format.</param>
/// <returns>
/// Depending on <see cref="Event.End"/> and <see cref="Event.HasTime"/>:
/// <list type="bullet">
/// <item><description>No end: <c>19:30 Uhr</c></description></item>
/// <item><description>Same day: <c>19:30–21:00 Uhr</c></description></item>
/// <item><description>Several days: <c>09:00 Uhr bis Sonntag, 11. Oktober 2026, 13:00 Uhr</c></description></item>
/// <item><description>Several days, no time: <c>bis Sonntag, 11. Oktober 2026</c></description></item>
/// <item><description>One day, no time: an empty string; the sheet says it all.</description></item>
/// </list>
/// </returns>
/// <remarks>
/// The sheet shows the start day only, so a multi-day event must name its last
/// day here, with the weekday, as the sheet does for the first one.
/// </remarks>
public static string Span(Event @event)
=> (@event.Start, @event.End, @event.HasTime) switch
{
(var start, { } end, var hasTime) when end.Date != start.Date
=> (hasTime ? Clock(start) + " Uhr " : "") + "bis " + Stamp(end, LongDate, hasTime),
(var start, null, true)
=> $"{Clock(start)} Uhr",
(var start, { } end, true)
=> $"{Clock(start)}–{Clock(end)} Uhr",
_ => "",
};
/// <summary>
/// The day column of a past event in the folded-up list on the events page.
/// </summary>
/// <param name="event">The event to format.</param>
/// <returns>
/// Day and full month, depending on <see cref="Event.End"/>:
/// <list type="bullet">
/// <item><description>One day: <c>13. November</c></description></item>
/// <item><description>Several days in one month: <c>8.–9. Februar</c></description></item>
/// <item><description>Across a month: <c>30. Januar – 2. Februar</c></description></item>
/// <item><description>Across a year: <c>30. Dezember – 2. Januar 2027</c></description></item>
/// </list>
/// </returns>
/// <remarks>
/// The list is grouped by the start year under its own small heading, so the
/// year is only named where the end leaves that year. The weekday is left out
/// to keep the column short; the time follows the title (<see cref="PastTime"/>).
/// </remarks>
public static string PastDays(Event @event)
=> (@event.Start, @event.End) switch
{
(var start, { } end) when end.Year != start.Year
=> $"{start.ToString(DayMonth, Cultures.German)} – {end.ToString(ShortDate, Cultures.German)}",
(var start, { } end) when end.Month != start.Month
=> $"{start.ToString(DayMonth, Cultures.German)} – {end.ToString(DayMonth, Cultures.German)}",
(var start, { } end) when end.Day != start.Day
=> $"{start.Day.ToString(Cultures.German)}.–{end.ToString(DayMonth, Cultures.German)}",
(var start, _)
=> start.ToString(DayMonth, Cultures.German),
};
/// <summary>
/// The time after the title of a past event in the folded-up list.
/// </summary>
/// <param name="event">The event to format.</param>
/// <returns>
/// Depending on <see cref="Event.HasTime"/> and <see cref="Event.End"/>:
/// <list type="bullet">
/// <item><description>No time: an empty string; nothing is shown.</description></item>
/// <item><description>No end: <c>19:00 Uhr</c></description></item>
/// <item><description>Same day: <c>19:00–21:00 Uhr</c></description></item>
/// <item><description>Several days: the start time only, <c>09:00 Uhr</c></description></item>
/// </list>
/// </returns>
/// <remarks>
/// For several days the day column already shows the span; a time range beside
/// it would read as the daily hours, so only the start time is given.
/// </remarks>
public static string PastTime(Event @event)
=> (@event.HasTime, @event.End) switch
{
(false, _) => "",
(true, { } end) when end.Date != @event.Start.Date => $"{Clock(@event.Start)} Uhr",
_ => Span(@event),
};
/// <summary>
/// Formats the clock time of a point in time.
/// </summary>
/// <param name="value">The date and time.</param>
/// <returns>The time as e.g. <c>19:30</c>.</returns>
private static string Clock(DateTime value)
=> value.ToString("HH:mm", Cultures.German);
/// <summary>
/// Formats a single point in time as a date with an optional time.
/// </summary>
/// <param name="value">
/// The date and time to format.
/// </param>
/// <param name="dateFormat">
/// The date format, e.g. <see cref="LongDate"/>.
/// </param>
/// <param name="withTime">
/// <see langword="true"/> to append the time, e.g. <c>, 19:30 Uhr</c>.
/// </param>
/// <returns>
/// The formatted date, with the time if requested.
/// </returns>
private static string Stamp(DateTime value, string dateFormat, bool withTime)
=> withTime
? value.ToString(dateFormat + ", HH:mm", Cultures.German) + " Uhr"
: value.ToString(dateFormat, Cultures.German);
}