Program.cs

The complete calendar program:

using System.Collections.Immutable;
using Funcky.Extensions;
using Funcky;
using Funcky.Monads;
using static System.Globalization.CultureInfo;
using static Funcky.Functional;

namespace Calendar;

internal static class StringExtensions
{
    public static string Center(this string text, int width)
        => (width - text.Length) switch
        {
            0 => text,
            1 => $" {text}",
            _ => Center($" {text} ", width),
        };
}

internal class Program
{
    private const int FirstDayOfTheWeek = 0;
    private const int MonthsPerRow = 3;
    private const int DaysInAWeek = 7;
    private const int WidthOfDay = 3;
    private const int WidthOfAWeek = WidthOfDay * DaysInAWeek;
    private const string MonthNameFormat = "MMMM";

    private static void Main(string[] args)
        => Console.WriteLine(CreateCalendarString(ExtractYear(args).GetOrElse(DateTime.Now.Year)));

    private static Option<int> ExtractYear(IEnumerable<string> args)
        => args.FirstOrNone()
            .AndThen(ParseExtensions.ParseInt32OrNone);

    private static string CreateCalendarString(int year)
        => Sequence.Successors(JanuaryFirst(year), NextDay)
            .TakeWhile(IsSameYear(year))
            .AdjacentGroupBy(day => day.Month)
            .Select(LayoutMonth)
            .Chunk(MonthsPerRow)
            .Select(EnumerableExtensions.Transpose)
            .Select(JoinLine)
            .SelectMany(Identity)
            .JoinToString(Environment.NewLine);

    private static DateOnly NextDay(DateOnly day)
        => day.AddDays(1);

    private static DateOnly JanuaryFirst(int year)
        => new(year: year, month: 1, day: 1);

    private static Func<DateOnly, bool> IsSameYear(int year)
        => day
            => day.Year == year;

    private static IEnumerable<string> JoinLine(IEnumerable<IEnumerable<string>> sequence)
        => sequence.Select(t => t.JoinToString(" "));

    private static IEnumerable<string> LayoutMonth(IEnumerable<DateOnly> month)
        => ImmutableList<string>.Empty
            .Add(CenteredMonthName(month))
            .AddRange(FormatWeeks(month))
            .Add(new string(' ', WidthOfAWeek));

    private static IEnumerable<string> FormatWeeks(IEnumerable<DateOnly> month)
        => month
            .AdjacentGroupBy(GetWeekOfYear)
            .Select(FormatWeek);

    private static string FormatWeek(IGrouping<int, DateOnly> week)
        => PadWeek(week.Select(FormatDay).ConcatToString(), week);

    private static string FormatDay(DateOnly day)
        => $"{day.Day,WidthOfDay}";

    private static string PadWeek(string formattedWeek, IEnumerable<DateOnly> week)
        => StartsOnFirstDayOfWeek(week)
            ? $"{formattedWeek,-WidthOfAWeek}"
            : $"{formattedWeek,WidthOfAWeek}";

    private static bool StartsOnFirstDayOfWeek(IEnumerable<DateOnly> week)
        => NthDayOfWeek(week.First().DayOfWeek) is FirstDayOfTheWeek;

    private static int NthDayOfWeek(DayOfWeek dayOfWeek)
        => (dayOfWeek + DaysInAWeek - CurrentCulture.DateTimeFormat.FirstDayOfWeek) % DaysInAWeek;

    private static int GetWeekOfYear(DateOnly dateTime)
        => CurrentCulture
            .Calendar
            .GetWeekOfYear(
                dateTime.ToDateTime(default),
                CurrentCulture.DateTimeFormat.CalendarWeekRule,
                CurrentCulture.DateTimeFormat.FirstDayOfWeek);

    private static string CenteredMonthName(IEnumerable<DateOnly> month)
        => month
            .First()
            .ToString(MonthNameFormat)
            .Center(WidthOfAWeek);
}