Nulah.PowerShell/PowershellModule/Calendar/GetCalendarCommand.cs
Scott 274d279732 feat: Add Get-Calendar command and Debug harnesses
- add Get-Calendar command
- add initial debug harness for local debug
2026-07-19 00:32:32 +00:00

173 lines
No EOL
6 KiB
C#

using System;
using System.Globalization;
using System.Management.Automation;
namespace PowershellModule.Calendar
{
[Cmdlet(VerbsCommon.Get, Noun)]
public class GetCalendarCommand : PSCmdlet
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
[Parameter(
Mandatory = false,
Position = 0)]
public string Date { get; set; }
[Parameter(
Mandatory = false,
Position = 1)]
public string MarkedDaySymbol { get; set; }
[Parameter(
Mandatory = false,
Position = 2)]
[ValidateSet("Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday")]
// pass a type in to get dynamic types
// NOTE: the generator gets called for _each_ character input, so ensure the values are idempotent for every
// keydown (and fast), or cached
// [ValidateSet(typeof(StartDayOfWeekGenerator))]
public string StartOfWeek { get; set; }
[Parameter(
Mandatory = false,
Position = 3)]
public SwitchParameter AlignRight { get; set; }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
// Guaranteed to be initialised in BeginProcessing. If it's not, something is cooked in some other way
private ModuleCore.Calendar.Calendar _calendar = null!;
private const string Noun = "Calendar";
public const string FullName = $"{VerbsCommon.Get}-{Noun}";
private DateTime _dateToRender = DateTime.Now;
private int _leftPadding;
protected override void BeginProcessing()
{
// This is the first point in script execution where we'll have parameters populated
_calendar = new(
MarkedDaySymbol,
DayOfWeekFromString(StartOfWeek)
);
if (!string.IsNullOrWhiteSpace(Date))
{
_dateToRender = DateTime.ParseExact(Date, CultureInfo.CurrentCulture.DateTimeFormat.GetAllDateTimePatterns(), null);
}
if (AlignRight)
{
_leftPadding = Host.UI.RawUI.WindowSize.Width;
}
}
/// <summary>
/// Returns a <see cref="DayOfWeek"/> from a string.
/// <para>
/// Defaults to the calendars default start of week on null or invalid values.
/// </para>
/// </summary>
/// <param name="dayOfWeek"></param>
/// <returns></returns>
private static DayOfWeek DayOfWeekFromString(string? dayOfWeek)
{
if (dayOfWeek is null)
{
return ModuleCore.Calendar.Calendar.DefaultStartOfWeek;
}
return dayOfWeek.ToLowerInvariant() switch
{
"sunday" => DayOfWeek.Sunday,
"monday" => DayOfWeek.Monday,
"tuesday" => DayOfWeek.Tuesday,
"wednesday" => DayOfWeek.Wednesday,
"thursday" => DayOfWeek.Thursday,
"friday" => DayOfWeek.Friday,
"saturday" => DayOfWeek.Saturday,
_ => ModuleCore.Calendar.Calendar.DefaultStartOfWeek
};
}
protected override void ProcessRecord()
{
var calendar = _calendar.RenderCalendar(_dateToRender);
// From _very_ basic testing, string split allocates the same as making a span and iterating over it.
// I'm assuming this is because a lot of the span code is what string.Split() does internally and the rest
// is trivially optimised out.
// Not too fussed currently but it's most likely because Pad methods also allocate a string, so they end up
// equivalent at the end
// OutputCalendarStringSplit(calendar);
OutputCalendar(calendar);
base.ProcessRecord();
}
private void OutputCalendarStringSplit(string calendar)
{
var splitCalendarLines = calendar.Split(Environment.NewLine);
foreach (var splitLines in splitCalendarLines)
{
// Have to output a newline character here
WriteObject(splitLines.PadLeft(_leftPadding));
}
}
private void OutputCalendar(string calendar)
{
// Work on a span - we avoid using string.Split() as this allocates on certain runtimes
var calendarSpan = calendar.AsSpan();
// Get the end of line for the calendar. We use Environment.NewLine as the calendar is built up using
// StringBuilder which uses Environment.NewLine for new lines so this should be consistent across operating systems
var firstNewlineIndex = calendarSpan.IndexOf(Environment.NewLine);
// Calculate how many lines the calendar is from how many new lines we have.
// Sure we could count the number of \r\n are present, but the last line might not end with
// a new line.
// Conveniently enough, int division works out in our favor here
var lines = calendarSpan.Length / firstNewlineIndex;
// We need to account for the size of the newline so we have an accurate length when outputting each line
// of the calendar below
var newLineLength = firstNewlineIndex + Environment.NewLine.Length;
for (int i = 0; i < lines; i++)
{
// The start of each calendar line offset starts at a position including the length of the newline,
// so in effect the start of the line is the length of the previous line output, plus however many
// characters the newline was that weren't output as we're taking control there
var startOfLine = i * newLineLength;
// End of the line is exclusive of any newline characters
var endOfLine = startOfLine + firstNewlineIndex;
var calendarLine = calendarSpan[startOfLine .. endOfLine];
WriteObject(calendarLine.ToString().PadLeft(_leftPadding));
}
}
}
// Was here to test dynamic day names but most people using the console will be using english command names
// so there's at least some assumption that they understand days of the week in english.
// It sucks but it remains at least consistent with the argument being in english as well.
// public class StartDayOfWeekGenerator : IValidateSetValuesGenerator
// {
// private string[] _cached { get; set; } = new string[7];
//
// public string[] GetValidValues()
// {
// if (_cached is null)
// {
// // _cached = Enumerable.Range(0, r.Next(3, 63)).Select(x => x.ToString()).ToArray();
// }
//
// return _cached;
// }
// }
}