Table of Contents

Authoring with the notable-date builder

Bodu.Globalization.Calendar.Builder is a fluent, chainable API for authoring notable-date documents in C#. It is the programmatic peer of the XML / JSON authoring path in Authoring notable date rules: both produce the same notable-date document, feed the same resolution pipeline, and validate through the same loader - but the builder keeps everything in code, so documents can be composed, cloned, serialized, and round-tripped without writing XML by hand.

For the vocabulary it assumes (document vs. resource, definition vs. rule, strategy, adjustment policy, nominal vs. observed) read Core concepts first. For the full type list see the Bodu.Globalization.Calendar.Builder reference.

The builder hierarchy

Each level is reached through a nested Add*(…, configure) callback that hands you the child builder and returns the parent for chaining:

Builder Configures Obtained from
NotableDateDocumentBuilder The whole <NotableDateResource> document NotableDateDocumentBuilder.Create(resourceId)
ResolutionPolicyBuilder The <ResolutionPolicy> WithResolutionPolicy(configure)
AdjustmentPolicyBuilder A reusable <AdjustmentPolicy> AddAdjustmentPolicy(id, configure)
NotableDateDefinitionBuilder One <NotableDate> concept AddNotableDate(id, displayName, category, configure)
NotableDateRuleBuilder One <Rule> AddRule(id, configure)
ImportBuilder / ImportUseBuilder <Imports> from the common catalogues AddImport(resource, configure)
OverrideBuilder ID-targeted <Overrides> AddOverride(configure)

Building a document

Create starts an empty document; AddNotableDate adds a concept, and each concept holds one or more rules:

NotableDateDocumentBuilder builder = NotableDateDocumentBuilder.Create("contoso.holidays")
    .WithMetadata(name: "Contoso holidays", description: "Company observances.")
    .AddNotableDate("new-years-day", "New Year's Day", NotableDateCategory.PublicHoliday, d => d
        .AsNonWorkingByDefault()
        .AddRule("default", r => r.ForTerritory("US").Fixed(1, 1)));

Rules and strategies

NotableDateRuleBuilder sets the rule's scalars (WithPriority, WithCategory, AsNonWorking, WithDurationDays, WithComment, AddTag), its applicability (ForCalendar, ForTerritory / ForTerritories, FromYear, ToYear, EveryYears, AnchorYear, OnlyYears, ExceptYears), its adjustment references (WithAdjustment), and exactly one occurrence source - a single-date strategy or a frequency-based recurrence source. The most common:

r.Fixed(1, 1);                                                       // 1 January (month name or number)
r.DayOfWeekInMonth(11, DayOfWeek.Thursday, WeekOrdinal.Fourth);     // 4th Thursday in November
r.WeekdayNearDate(5, 24, DayOfWeek.Monday, WeekdayProximity.OnOrBefore);
r.RelativeWeekdayInMonth(11, DayOfWeek.Monday, WeekOrdinal.First, DayOfWeek.Tuesday, WeekdayProximity.After);
r.OffsetFromRule("easter-sunday", -2, ruleRef: "default");          // Good Friday = Easter − 2
r.Algorithm("western-easter");                                       // a named algorithm key

The remaining single-date strategies (OrdinalDayOfMonth, DayOfYear, IsoWeekDate, WeekdayNearRule, NthWeekdayFromRule, WorkingDayOffsetFromRule, WorkingDayInMonth) and the recurrence sources (DailyInterval, Weekly, MonthlyDay, MonthlyWeekday) follow the same shape. Selecting a second strategy on the same rule throws InvalidOperationException - each rule commits to one. See the strategy reference for the full catalogue and Date calculation algorithms for the strategy semantics and the <Algorithm> keys.

Adjustment policies

A weekend-substitution or "move to the next working day" shift is authored once as a reusable policy and referenced from any rule by id (there are no inline per-rule adjustments):

builder
    .AddAdjustmentPolicy("weekend-to-monday", a => a
        .WithDescription("Observe weekend holidays on the next working day.")
        .When(AdjustmentTrigger.IfWeekend)
        .Then(AdjustmentAction.MoveToNextWorkingDay)
        .Emit(Bodu.Globalization.Calendar.RangeResolution.EmissionMode.ObservedOnly))
    .AddNotableDate("anzac-day", "Anzac Day", NotableDateCategory.PublicHoliday, d => d
        .AsNonWorkingByDefault()
        .AddRule("default", r => r.ForTerritory("AU").Fixed(4, 25).WithAdjustment("weekend-to-monday")));

AdjustmentPolicyBuilder also exposes the trigger modifiers (OnTriggerWeekdays, WithTriggerMonth, …), action modifiers (WithActionDays, WithMaxSearchDays, SkipWeekends, WithReplacementRule, …), emission (WithReason, EmitNonWorking), handler parameters (WithParameter), and a WithScope(configure) callback over AdjustmentScopeBuilder. See Observance adjustment rules for the full trigger / action / emission catalogues.

Importing the common catalogues

AddImport pulls concepts from the bundled common catalogues; Use cherry-picks and re-scopes them. ImportUseBuilder exposes As(alias) (re-id the imported concept), ForTerritory, WithCategory, AsNonWorking, and WithAdjustment:

builder.AddImport("global-core", i => i
    .Use("new-years-day", u => u
        .As("us-new-years-day")           // re-id to avoid clashing with a local concept
        .ForTerritory("US")
        .WithAdjustment("weekend-to-monday")));

An AddImport with no Use calls imports every concept the catalogue defines. Because the JSON subset cannot model imports, a document that calls AddImport serializes only as XML - see the note under Materializing.

Overrides

AddOverride authors ID-targeted edits applied at load time. OverrideBuilder offers three operations - AddRule (add a rule to an existing concept), PatchRule (replace an existing rule), and RemoveRule (suppress one):

builder.AddOverride(o => o
    .RemoveRule("boxing-day", "default")
    .PatchRule("anzac-day", "default", r => r.ForTerritory("AU").Fixed(4, 25).WithAdjustment("weekend-to-monday"))
    .AddRule("company-founding-day", "hq", r => r.ForTerritory("US").Fixed(6, 15)));

Resolution policy

builder.WithResolutionPolicy(p => p
    .WithDuplicatePolicy(Bodu.Globalization.Calendar.RangeResolution.DuplicatePolicy.KeepFirst)
    .WithWorkingWeek(WeekPattern.MondayToFriday));

Materializing, serializing, and saving

A finished builder produces the document in several forms:

// 1. A built, validated resource - ready for a NotableDateService.
NotableDateResource resource = builder.Build();                  // Build(resolver) when the document imports
NotableDateService  service  = new NotableDateService(resource);

// 2. An INotableDateResourceProvider (for the reloadable service / DI).
INotableDateResourceProvider provider = builder.ToProvider();

// 3. Serialized text - full-fidelity XML, or the JSON subset.
string xml  = builder.ToXml();      // also ToXDocument()
string json = builder.ToJson();     // also ToJsonObject()

// 4. Straight to a file (format inferred from the extension).
builder.Save("holidays.xml");
builder.Save("holidays.json");

// …or pin the format explicitly when the extension does not imply it:
builder.Save("holidays.txt", NotableDateDocumentFormat.Xml);

Build() serializes to XML and loads through NotableDateResourceLoader, so the built resource is exactly what the runtime would load - and the same validation applies (Build() throws NotableDateValidationException on an invalid document).

Note

XML is the full-fidelity format. ToJson() / Save(*.json) emit the narrower JSON subset and throw NotSupportedException when the document uses a feature the JSON schema cannot model - imports, a non-Gregorian calendar, an XML-only trigger/action value, handler parameters, or scope year-bounds. Serialize those documents as XML.

Round-tripping and cloning

FromXml / FromJson parse a document back into a builder for editing, and Load(path) reads a file by extension - so you can load, mutate, and re-save:

NotableDateDocumentBuilder edited = NotableDateDocumentBuilder.Load("holidays.xml");
edited.AddNotableDate("juneteenth", "Juneteenth", NotableDateCategory.PublicHoliday, d => d
    .AddRule("default", r => r.ForTerritory("US").Fixed(6, 19)));
edited.Save("holidays.xml");

NotableDateDocumentBuilder copy = edited.Clone();   // deep, independent copy

Round-tripping carries precise guarantees - builder-canonical XML is byte-stable, JSON is identity within its subset, and both formats resolve the same occurrences. Builder round-trip guarantees states the full contract, including the XML → JSON lossiness boundary.

Where to go next