feat: Add Get-Calendar command and Debug harnesses
- add Get-Calendar command - add initial debug harness for local debug
This commit is contained in:
parent
468f3c64ce
commit
274d279732
15 changed files with 938 additions and 1 deletions
259
ModuleCore/Calendar/Calendar.cs
Normal file
259
ModuleCore/Calendar/Calendar.cs
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
using System.Globalization;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Text;
|
||||
|
||||
namespace ModuleCore.Calendar;
|
||||
|
||||
public sealed class Calendar
|
||||
{
|
||||
public const string DefaultMarkedOffSymbol = "x";
|
||||
public const DayOfWeek DefaultStartOfWeek = DayOfWeek.Monday;
|
||||
|
||||
private readonly string _markedOffSymbol;
|
||||
private readonly DayOfWeek _startOfWeek;
|
||||
|
||||
/// <summary>
|
||||
/// Holds configured day abbreviations, based on <see cref="CultureInfo"/>.
|
||||
/// <para>
|
||||
/// Using english as the culture: Sunday = Sun, Monday = Mon and so on. This lookup also starts with
|
||||
/// Sunday as the first day to match the <see cref="DayOfWeek"/> enum starting with Sunday.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private readonly List<string> _daysOfWeekLookup;
|
||||
|
||||
/// <summary>
|
||||
/// Used to work out how to pad columns for each day, based on the longest abbreviated day
|
||||
/// </summary>
|
||||
private readonly int _dayWidth;
|
||||
|
||||
private readonly CultureInfo _cultureInfo;
|
||||
|
||||
private readonly string _rolloverDaySymbol = "";
|
||||
|
||||
public Calendar(string? markedDaySymbol = null, DayOfWeek? startOfWeek = null)
|
||||
{
|
||||
_markedOffSymbol = markedDaySymbol ?? DefaultMarkedOffSymbol;
|
||||
_startOfWeek = startOfWeek ?? DefaultStartOfWeek;
|
||||
|
||||
// Generate localised day of week lookup table
|
||||
_daysOfWeekLookup = new List<string>(7);
|
||||
_dayWidth = _markedOffSymbol.Length;
|
||||
// TODO: make this passed in from configuration, and if its invalid default it to
|
||||
// CultureInfo.CurrentCulture
|
||||
// TODO: test if its possible to determine if a culture string is invalid and what happens
|
||||
_cultureInfo = new CultureInfo("en-AU");
|
||||
for (var i = 0; i < 7; i++)
|
||||
{
|
||||
var localisedDayAbbreviation = _cultureInfo.DateTimeFormat.GetAbbreviatedDayName((DayOfWeek)i);
|
||||
|
||||
// For unicode languages (such as Japanese and Chinese), the length of a day will be reported as 1,
|
||||
// but will display in a way that makes them look short or offset.
|
||||
// eg:
|
||||
// ┌───────────────────────────┐
|
||||
// │ 7月 │
|
||||
// ├───┬───┬───┬───┬───┬───┬───┤
|
||||
// │ 月 │ 火 │ 水 │ 木 │ 金 │ 土 │ 日 │
|
||||
// ├───┴───┴───┴───┴───┴───┴───┤
|
||||
// There's not really an easy fix I know of (yet) to resolve this (and I'm not really too bothered by it yet)
|
||||
// so unfortunately it has to stay as a known bug as is currently
|
||||
if (localisedDayAbbreviation.Length > _dayWidth)
|
||||
{
|
||||
_dayWidth = localisedDayAbbreviation.Length;
|
||||
}
|
||||
|
||||
_daysOfWeekLookup.Add(localisedDayAbbreviation);
|
||||
}
|
||||
|
||||
// Increase the width by 2 to include a space on either side
|
||||
_dayWidth += 2;
|
||||
}
|
||||
|
||||
public string RenderCalendar(DateTime month)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
// Generate the header for the month/days of week
|
||||
GenerateHeader(sb, month);
|
||||
|
||||
var calendarForMonth = GenerateCalendarForMonth(month, _markedOffSymbol, _startOfWeek, _rolloverDaySymbol);
|
||||
|
||||
for (var i = 0; i < calendarForMonth.WeeksInMonth; i++)
|
||||
{
|
||||
sb.Append("│");
|
||||
for (var j = 0; j < 7; j++)
|
||||
{
|
||||
var index = j + i * 7;
|
||||
// We align days to the right of their box, 1 off from the border.
|
||||
// Sure I could center them, but honestly this is better to read.
|
||||
sb.Append(calendarForMonth.Calendar[index].PadLeft(_dayWidth - 1));
|
||||
sb.Append(" │");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Cap the calendar off
|
||||
GenerateSpacer(sb, "└", "─", "┴", "┘", true);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void GenerateHeader(StringBuilder sb, DateTime month)
|
||||
{
|
||||
// Top border for the calendar, with padder and seperator the same as there's no columns immediately
|
||||
// under it
|
||||
GenerateSpacer(sb, "┌", "─", "─", "┐");
|
||||
|
||||
var monthString = month.ToString("MMMM yyyy", _cultureInfo);
|
||||
// total width of the calendar is width of the localised day calculated in the constructor, plus 7 extra
|
||||
// to account for separators for each day.
|
||||
// It shouldn't need to be said that the 7 here stands for days in a week
|
||||
var calendarWidth = _dayWidth * 7 + 7;
|
||||
// Calculate the offsets for
|
||||
var offsets = (
|
||||
left: Math.Floor(calendarWidth / 2.0 + monthString.Length / 2.0),
|
||||
right: Math.Ceiling(calendarWidth / 2.0 - monthString.Length / 2.0)
|
||||
);
|
||||
|
||||
sb.Append("│")
|
||||
.Append(monthString.PadLeft((int)offsets.left))
|
||||
.AppendLine("│".PadLeft((int)offsets.right));
|
||||
|
||||
// Top border for the week
|
||||
GenerateSpacer(sb, "├", "─", "┬", "┤");
|
||||
|
||||
// Work out the week display based on start of week
|
||||
var d = 0;
|
||||
do
|
||||
{
|
||||
// Index into the day of week, wrapping if needed from the configured start of the week.
|
||||
// The DayOfWeek enum starts with Sunday, so by default the start of the week is 0,
|
||||
// But most calendars visually start with Monday as the start of the week which is our default start
|
||||
// of week.
|
||||
// We also pad the string to the width of the day (minus 2 for spaces either side of the day), if
|
||||
// the marked day symbol is a long string.
|
||||
// Why did I decide to support arbitrary length marked days? Why not?
|
||||
sb.Append($"│ {_daysOfWeekLookup[(d + (int)_startOfWeek) % 7].PadLeft(_dayWidth - 2)} ");
|
||||
d++;
|
||||
} while (d < 7);
|
||||
|
||||
// End the week block
|
||||
sb.AppendLine("│");
|
||||
// bottom border
|
||||
GenerateSpacer(sb, "├", "─", "┼", "┤");
|
||||
}
|
||||
|
||||
private void GenerateSpacer(StringBuilder sb, string leftCap, string padder, string seperator, string rightCap, bool noNewLine = false)
|
||||
{
|
||||
sb.Append(leftCap);
|
||||
for (var i = 0; i < 7; i++)
|
||||
{
|
||||
for (var j = 0; j < _dayWidth; j++)
|
||||
{
|
||||
sb.Append(padder);
|
||||
}
|
||||
|
||||
if (i < 6)
|
||||
{
|
||||
sb.Append(seperator);
|
||||
}
|
||||
}
|
||||
|
||||
if (noNewLine)
|
||||
{
|
||||
sb.Append(rightCap);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(rightCap);
|
||||
}
|
||||
}
|
||||
|
||||
private MonthCalendar GenerateCalendarForMonth(DateTime month, string markedOffSymbol, DayOfWeek calendarStartOfWeek, string rolloverDaySymbol)
|
||||
{
|
||||
// TODO: use startOfWeek to offset the start of the calendar later
|
||||
|
||||
// Reset the month to the start
|
||||
var startOfMonth = new DateTime(month.Year, month.Month, 1);
|
||||
// If the visual start of the month is before the actual start day of the month, we need to visually shift the
|
||||
// calendar "back" a week, and pad appropriately.
|
||||
// Eg, if the start of the month is Sunday, but our visual start of the week is a Monday, then we need to
|
||||
// make a new week to display Monday as the start, and then pad blank days to Sunday.
|
||||
// This could probably be calculated better to avoid annoying to read logic but I'm drunk at this moment so the
|
||||
// brain ain't there.
|
||||
// For what its worth, I wasn't drunk for the rest of the code in this file, as hard as that is to believe!
|
||||
var daysToPadToStartMonth = startOfMonth.DayOfWeek >= calendarStartOfWeek
|
||||
? startOfMonth.DayOfWeek - calendarStartOfWeek
|
||||
: 7 - (calendarStartOfWeek - startOfMonth.DayOfWeek);
|
||||
|
||||
var daysInMonth = DateTime.DaysInMonth(month.Year, month.Month);
|
||||
// TODO: a lot of this could probably be simplified by simply taking the start day of the week, and then
|
||||
// based on where it would be in the first week, work out if the month is 4, 5 or 6 weeks from that.
|
||||
// all this needs is a pre-calculated table and that's it.
|
||||
// Weeks have 7 days so there's only a fixed permutation of weeks based on the day of the week a month
|
||||
// starts on, and months have anywhere from 28 to 31 days
|
||||
// Increase the days by the previous months
|
||||
var paddedDaysInMonth = daysInMonth + daysToPadToStartMonth;
|
||||
// How many days to add to the end to make the absolute number of days (including any
|
||||
// days that would visually roll over on a calendar)
|
||||
// eg, if there are 2 "roll over" days from the previous month and this month has 31 days,
|
||||
// Work out how many days we have left from 7, then get the _final_ number of days to make it a multiple
|
||||
// _of_ 7.
|
||||
// So this results in (33%7) == 5 - 7 == 2 + 33 == 35 % 7 == 0 which means we have a "complete" month
|
||||
// including next month roll over.
|
||||
// Visually we won't be showing those roll over days but it makes things easier if I decide to later.
|
||||
// If the days in the month plus padded days is already a multiple of 7, we don't need to add any more days.
|
||||
var daysToPadToEndMonth = paddedDaysInMonth % 7 != 0
|
||||
? 7 - paddedDaysInMonth % 7
|
||||
: 0;
|
||||
paddedDaysInMonth += daysToPadToEndMonth;
|
||||
// Round to the nearest week after padding for the previous months days.
|
||||
// Naively allowing a loss of precision via int division because the paddedDaysInMonth should always
|
||||
// be a multiple of 7.
|
||||
// "should always" is going to be found not as true later
|
||||
var weeksInMonth = paddedDaysInMonth / 7;
|
||||
|
||||
var cal = new List<string>();
|
||||
for (var i = 0; i < daysToPadToStartMonth; i++)
|
||||
{
|
||||
cal.Add(rolloverDaySymbol);
|
||||
}
|
||||
|
||||
var today = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
|
||||
// If we're in the same year and month, any days already elapsed should be marked off,
|
||||
// otherwise we display all days.
|
||||
// If the DateTime we're rendering is in the past we render all days as marked off
|
||||
// TODO: maybe don't do that and display the calendar differently
|
||||
var hideElapsedDays = today.Year == month.Year && today.Month == month.Month;
|
||||
|
||||
for (var i = 1; i < daysInMonth + 1; i++)
|
||||
{
|
||||
// if we're marking off elapsed days and we're not today or ahead, 'mark' it off
|
||||
if (hideElapsedDays && i < today.Day)
|
||||
{
|
||||
cal.Add(markedOffSymbol);
|
||||
}
|
||||
else
|
||||
{
|
||||
cal.Add($"{i}");
|
||||
}
|
||||
}
|
||||
|
||||
// pad the rollover days at the end of the calendar
|
||||
for (var i = 0; i < daysToPadToEndMonth; i++)
|
||||
{
|
||||
cal.Add(rolloverDaySymbol);
|
||||
}
|
||||
|
||||
return new MonthCalendar
|
||||
{
|
||||
Calendar = cal,
|
||||
WeeksInMonth = weeksInMonth
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class MonthCalendar
|
||||
{
|
||||
public List<string> Calendar { get; set; }
|
||||
public int WeeksInMonth { get; set; }
|
||||
}
|
||||
Loading…
Reference in a new issue