Implementation progress — 3 November 2026. cHolidayXML.pas now supports all 22 active rule types exposed by the supplied holiday editor. Olivier has compiled and exercised the codec locally, and reports successful round trips for every family. Database integration and the full XML-loaded generation comparison remain pending. This topic remains In Progress; serialization coverage is not a claim that production persistence has been switched to XML.
Scope: a complete recurring holiday, including its name, abbreviations, presentation, recurrence settings, observances, date lists and exceptions. THolidaysSetRec and external calendar/location records remain separately stored resources; a holiday contains references to them, not embedded copies of their full definitions. One-time dates in OneTimeDates are outside this THoliday format.
Required production integration (not yet connected): XV4 reads the legacy fields/BLOBs when the XML memo is SQL NULL, empty, or contains only spaces, tabs and line breaks; otherwise it reads XML. Nonblank malformed XML is an error and never falls back to legacy fields. This selection does not use TScript.LoadVersion. The original nonblank text is passed to the reader without trimming its payload. Every normal save writes XML and legacy data from the same validated holiday. XE3 only reads the worldwide database and continues using the legacy representation. Existing calculation semantics are retained.
Diary generation already visits every rule before discovering date-less years. The optional conversion/synchronization operation can therefore run at the per-rule load stage in that existing traversal, including rules with no dates. It must also refresh legacy fields from already-present XML, so SQL/regex XML edits reach the XE3 updater. There is no need to invent a second traversal solely for coverage.
The tables retain the complete source-field inventory and describe the current XML mapping, with remaining compatibility questions explicitly marked. The XML format separates settings from the binary layout of THolidayRule. The existing variant record is still used internally; replacing it later must retain an explicit legacy conversion for XE3.
Completed tests and next acceptance check
The harness loads a complete temporary THoliday from the database (including ProvinceID), updates its sorting keys and serializes it to CurrentHoliday. It creates a different THoliday, supplies ID, CountryID, DateModified, RN1 and RN2 as destination context, reads the XML and serializes that object to Restored. XMLText and TStringList.Text are reassigned before the second write. These are two object serializations, not two writes of the same saved string.
Direct LocalName and Abreviation comparisons test the actual in-memory text. Binary comparison of paired files checks canonical save → read → save stability. The five context fields are copied, not tested as XML-restored data. Matching XML cannot detect a setting omitted by both writer and reader, and does not independently prove identical calculated occurrences.
Reported local results: the earlier full main-database batch of 8,907 pairs matched binary-wise; the French client database, including school-vacation date ranges, also matched. Subsequent family tests completed through Vietnamese, Hindu, Tibetan/Bhutanese/Mongolian, Burmese, Egyptian, Zoroastrian, Persian, Sikh, SouthEastAsian, Sun and Lunisolar. All 22 active families have passed individual tests. A final combined run using the latest code remains a separate regression step.
For acceptance, retain a legacy-loaded baseline and regenerate the worldwide holidays for 2023–2055 from stored XML, comparing approximately 130,000 dates and names. Use the same input database and generation settings, with no intervening holiday edits. Confirm every tested rule loads nonblank XML so fallback cannot conceal missing conversion. Use empty comparison folders to exclude stale files. Verify regenerated legacy storage for XE3 separately.
Approximate sorting keys: UpdateRn1Rn2Values
THoliday.UpdateRn1Rn2Values extracts the RN1/RN2 derivation from the form. Call it before complete persistence; SaveToXML itself must remain a serialization operation. These keys approximate chronological display order, so base calendar dates are sufficient: weekend moves, conflict handling and exceptions need not be applied.
The revised hrEgypt and hrZoroastrian branches use the direct calendar conversion routines from FCalculateFor, with the current year, instead of calling CalculateFor. This avoids populating DatesArray through full generation. Initialize gDateOfEgyptian and gDateOfZoroastrian to zero before their subtype cases. Use zoMonth/zoDay in the Parsi guard; the former egMonth/egDay names only worked because of shared variant-record positions. The final Rule.AddDaysToRule adjustment remains once at the end of the method. Olivier will apply the initialization corrections; they are not recorded as verified here.
Clearing inactive date collections remains in the UI save preparation where appropriate, not in the sorting helper. XML reads initialize inactive storage explicitly. This keeps sorting estimation separate from editing rule data and calculating occurrence arrays.
Document conventions and root layout
Root: holiday, with schemaVersion="1". This documentation revision does not change the schema version. Child order is deterministic on writing; readers locate singleton elements by exact name. Repeated text blocks retain document order. Dots are literal XML-name characters: use camelCase within concepts and dots for ownership/hierarchy. Common defaults are explicit; only the active rule family, subtype and reference branch are serialized.
Booleans are true/false. Enum values use fixed, case-sensitive English tokens maintained in explicit conversion tables, never UI captions or Ord-to-text shortcuts. Dates use YYYY-MM-DD in the Gregorian calendar for actual dates; calendar-specific month/day numbers remain in their named calendar branch. Integers use decimal without grouping. Longitude values use invariant decimal degrees with exactly three fractional digits, converted exactly to/from the existing integer thousandths. Calendar/location IDs are references, with 0 meaning the set default where that is the existing convention.
Structural sections under holiday: holiday.identity, holiday.name, holiday.abbreviation, holiday.presentation, holiday.synchronization, observance, rule, and info. Common rule groups include validity, frequency, occurrences, offset, duration, calendarSources, weekdays, conflict and exceptions where applicable, followed by the active family. A table entry listing several dotted leaves means separate elements, not a combined string.
Unknown authoritative elements, duplicate singleton elements, missing required fields, unrecognized enum tokens and unsupported schema versions are errors. Only whitelisted informational elements are ignored; an arbitrary info="true" attribute cannot make an authoritative setting optional. Empty lists are explicit. Two documented schema-1 compatibility defaults apply to older XML: missing observance.noneSpecified is derived from the real flags, and missing observance.legacyPreviousEve defaults to false.
Names, abbreviations and multiline text
Correct boundary rule: a line begins a variation only when its first three characters are >>>. The reference name substituted for <hol> can contain several #7-separated lines. A #7 alone does not mark a variation. Split the stored text into lines while preserving empty lines, including leading/trailing empty lines; gather the initial lines as the base name, then gather each >>>-initiated block until the next such line. Do not trim, reorder, deduplicate or interpret the variation language during serialization.
Implemented lossless structure: base contains one base.line node per #7-delimited line. Each variation is a separate nameVariation (or abbreviationVariation) node containing one or more line nodes. Its first line retains the original >>>condition=value text. Subsequent lines and their order are retained verbatim. The same structure is applied separately to names and abbreviations.
<holiday.name> |
Reconstruction joins all base and variation lines with #7 in their original order. An empty name is one empty base.line, and no variations is an empty variations container. A literal >>> inside a line is ordinary text. A nonempty legacy value starting with >>> before any base separator is an edge case for the existing evaluator: identify it during migration rather than silently inserting a leading #7 or changing its interpretation.
CDATA is preferred for free-text line payloads. Tabs and XML-legal whitespace inside a line remain text; #7 is represented by node boundaries. Other forbidden control characters must be reported with their code point and field, not emitted as invalid XML or silently removed. A later explicit character-node convention can be added if real data requires it. XML newline normalization means literal CR/CRLF within a line needs an explicit preservation policy; detect it in the migration audit instead of assuming that it is #7.
Xml.VerySimple.pas: verified support and integration requirements
Verified in the supplied unit: ntCData exists; ParseCData creates a child node containing the payload; Walk writes that node as a CDATA section. TXmlNode.Text is a stored property, not a computed concatenation of descendant text. A holiday text helper must therefore read/write the CDATA children explicitly. Find/FindNodes default to ntElement and do not select CDATA unless the requested types include it.
Walk concatenates CDATA delimiters around Node.Text without splitting an embedded ]]> terminator. A dedicated writer must split such payloads into adjacent CDATA sections, and the reader must concatenate those sections in order. For example, the text a]]>b can be written as <![CDATA[a]]]]><![CDATA[>b]]>. Do not read only the first CDATA child.
With indented output, Walk may insert a formatting newline before the end tag of an element containing CDATA. The line helper must distinguish structural indentation from payload. Canonical free-text line elements use CDATA-only payload children; ignore whitespace-only formatting outside those CDATA children and reject mixed substantive text/child elements. The implementation writes empty payloads as self-closing line elements. This avoids depending on a parser retaining an empty CDATA node while still preserving empty logical lines. Ordinary escaped text is also accepted; substantive ordinary text mixed with CDATA is rejected. Payload is not blanket-trimmed.
The supplied Escape/Unescape code has a custom control-character convention. Escape converts #7 to  and subsequently escapes the ampersand, so the wire form is &#007;. Unescape reverses this convention. That is not a standards-based representation of #7 and can collide with literal strings such as . The new holiday text helpers should use the structural line representation and CDATA rather than depend on this convention. Do not change the shared library globally as part of the holiday feature without regression testing scripts.
The supplied ParseTag closing-tag branch moves to Parent.Parent without comparing the closing tag name. Successful parsing by this unit is therefore not sufficient proof of well-formed XML. Before using the tree, add strict well-formedness validation, or explicitly harden the parser with regression coverage. Also validate the holiday schema and values. This is a requirement of the agreed error handling, not a claim that the entire existing parser has been audited.
THoliday: identity, text and presentation
Source member / storage |
Proposed XML element(s) |
Value and conversion |
ID |
info.holiday.id [info=true] |
Database ID_Holiday remains authoritative for identity. The reader treats this as explanatory only and retains the destination identity. Identity/remapping checks belong to the calling edit/copy/import operation. Do not treat an edit to explanatory identity as a key update. |
CountryID |
info.holiday.setId [info=true] |
Owning holidays-set ID (despite the member name). Destination row context supplies it; copying to another set is an explicit operation. |
ProvinceID |
holiday.identity.provinceId |
Preserve the stored numeric association. Load it explicitly: the supplied GetHolidayFromTable does not populate it. Resolve the SmallInt member versus unsigned table-field width before broad import. |
AncestorID |
holiday.identity.ancestorId |
Integer, 0 for no ancestor if that is the current table convention. Validate/remap when synchronizing or copying; preserve normal edit restrictions. |
DateModified |
info.holiday.modifiedDate [info=true] |
Derived from the legacy Observance database field: loader uses Max(35000, 30000 + Observance). This field is a date encoding, not the observance flags. Preserve existing unknown/old-date handling. Only semantic edits update modification bookkeeping. |
LocalName |
holiday.name.base.line; holiday.nameVariation.line |
Ordered structures described above. Complete reference name including multiline #7 content and all variation blocks. |
Abreviation |
holiday.abbreviation.base.line; holiday.abbreviationVariation.line |
Keep the source spelling Abreviation only in this mapping; use abbreviation in XML. Same lossless block rules as names. |
Formatting |
holiday.presentation.formatting.line |
Opaque existing formatting text, with explicit #7-separated line nodes if needed. Do not discard because the main form currently assigns an empty string; its format is outside this source bundle. |
ShowFerie |
holiday.presentation.watermark.enabled |
Boolean; the form treats this as the holiday watermark setting. |
AutoBytes |
holiday.presentation.symbols.line |
Existing symbol/autosticker text, preserved including order. No binary decoding invented; use text helpers and explicit #7 line boundaries if present. |
GraphicsPath |
holiday.presentation.image.path |
Text path, preserved, not dereferenced or embedded. Validate for representability in the legacy memo; no automatic path rewriting. |
ImageAlign |
holiday.presentation.image.alignment |
Confirmed holiday domain: 0 CenterNoStretch, 1 KeepAspectRatio, 2 Stretch, 3 TopLeft. The user confirms that shared enum values 4..6 are not available for holidays. Do not accept SpecificValues, ScaleToFill or None as holiday XML alignment values; report unsupported stored values instead of coercing them. |
ImageTransparent |
holiday.presentation.image.transparent |
Boolean. Legacy field ImageAlign = alignment ordinal + 100 × transparency flag; XML separates these settings. |
Priority |
holiday.presentation.priority |
Zero-based index 0..19, confirmed from cbPriority.Items and Hol.Priority := cbPriority.ItemIndex. The captions display 1..20; XML preserves the stored index. |
Ignored |
holiday.synchronization.ignored |
Boolean. This is a synchronization preference, not a decision to exclude all generated dates. Copy/synchronization policy may override it explicitly. |
Kept |
holiday.synchronization.kept |
Boolean. Preserve the existing synchronization preference; do not reinterpret it as calendar validity. |
RuleType |
rule.type |
Stable family token; numeric mapping appears in the rule-type table. Subtypes remain explicit inside the active family. |
Rule |
rule.* |
Composed fields are mapped individually below; never serialize the raw variant-record buffer. |
RuleOptions |
rule.* and observance.* |
Composed fields are mapped individually below; one authoritative XML location per logical setting. |
ArbitraryDates |
rule.arbitraryDates.dates / rule.exceptions |
Use a date-list structure for hrArbitrary and year-based exception structure for other supported recurring types. Never expose mixed marker/date integers. |
DateRanges |
rule.ranges.items |
List of independent ranges, each with actual start date, duration and seven weekday flags. |
RN1; RN2 |
info.sort.month; info.sort.day [info=true, optional] |
Derived approximate sorting values. Recompute in the shared persistence service; do not import as editable recurrence settings. |
DatesArray |
not serialized |
Calculated occurrences, including internal signs/markers, are runtime results. Optional diagnostic previews must be info-only and identify their year/context. |
Image alignment values — confirmed holiday subset
Ordinal |
Pascal enum |
Proposed XML token |
Current holiday editor |
0 |
pbaCenterNoStretch |
CenterNoStretch |
Center (no stretch) |
1 |
pbaHomeoStretch |
KeepAspectRatio |
Stretch (keep ratio) |
2 |
pbaFullStretch |
Stretch |
Full stretch |
3 |
pbaTopLeft |
TopLeft |
Top-Left (no stretch) |
4 |
pbaSpecificValues |
Not permitted |
Not supported for holidays |
5 |
pbaScaleToFill |
Not permitted |
Not supported for holidays |
6 |
pbaNone |
Not permitted |
Not supported for holidays |
The stale three-member comment in cHoliday is not a definition of the enum. The actual shared enum contains seven values, but the user confirms that holidays support only ordinals 0..3. This matches the four holiday-editor choices. The holiday XML domain therefore contains only CenterNoStretch, KeepAspectRatio, Stretch and TopLeft. Reject shared enum values 4..6 and all other out-of-domain values in holiday data with an error identifying the holiday and setting. This compatibility question is closed. For detailed image positioning, the user recommends slogans; the linked PictureBox Alignment documentation describes the fuller controls used by slogans, macros and SARAs.
Table columns outside THoliday
Column(s) |
Treatment |
NoSite |
Database row scope; retain the composite identity checks (NoSite, ID_Holiday). Not an editable setting inside XML. |
ReadOnly, Needed |
Operational columns not represented by THoliday in the supplied declaration. Keep in database/service context; do not overwrite from a freshly initialized THoliday. |
SzRule, SzRuleOptions, SzArbitraryDates, SzDateRanges |
Computed by legacy writers. No editable XML equivalents. |
BlobRule, BlobRuleOptions, BlobArbitraryDates, BlobDateRanges |
Compatibility output only after authoritative XML load; imported through old readers when XML is NULL or blank. |
AutoBytesVersion and any schema-specific columns |
The supplied table component also declares AutoBytesVersion. Audit the actual data dictionary and other consumers before deciding removal or XML inclusion. Preserve untouched operational columns. |
New XML memo |
Name and NexusDB field type remain to be selected consistently with script storage. NULL or blank selects legacy storage; a successfully migrated row contains nonblank XML. Add to SELECT, INSERT, UPDATE, cached dataset fields, direct table fields and migration/update schemas. |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.Validity |
rule.validity.mode |
Always, Beginning, Ending, FromTo, Except ↔ 0, 1, 2, 3, 4. Common node always written; non-applicable families use their existing neutral validity. |
Rule.Year1 |
rule.validity.fromYear / rule.validity.toYear |
Beginning: from=Year1. Ending: to=Year1. FromTo/Except: from=Year1. Always: neither bound is active. Use None for inactive XML bounds; never expose Year1 with two undocumented meanings. |
Rule.Year2 |
rule.validity.toYear |
FromTo/Except: to=Year2. Other modes: None. Each active bound is 1..9999. Reversed bounds are preserved, not reordered or rejected: FromTo then yields no eligible years; Except excludes no years. Inactive legacy slots are initialized deterministically. |
Rule.AddDaysToRule |
rule.offset.amount |
Signed integer, existing comment specifies -300..300. XML is the amount alone, not the table combined value. |
Rule.TypeOfDaysToAddToRule |
rule.offset.units |
Days, Weekdays, WeekdaysIncludingSaturday ↔ 0,1,2. Legacy table value = 1000 × ordinal + amount. Reader uses the existing +500 unpacking convention. |
Rule.HolDuration |
rule.duration.amount; rule.duration.units |
amount = HolDuration mod 100; units = HolDuration div 100. Tokens Days, Weekdays, WeekdaysIncludingSaturday ↔ 0,1,2. The implementation accepts amount 1..99 and units 0..2. The DFM spin editor allows 300 but the stored value is decoded by mod/div 100; wider intended amounts require a separate compatibility decision. Preserve calculation semantics, including first-day handling. Do not infer that this is an end date. |
RuleOptions.OnlyShowFirstOccurrence |
rule.occurrences.firstOnly |
Boolean; false default. |
RuleOptions.HolFreqNumber |
rule.frequency.intervalYears |
Positive integer; 1 default. Reject zero or negative. |
RuleOptions.HolFreqReference |
rule.frequency.referenceYear |
Integer reference year; 2000 default. Preserve even when interval=1. |
RuleOptions.Activated |
not editable; legacy normalization |
Legacy options representation flag, normally true since 2004. Old readers convert the previous options when necessary. XML reader constructs current options and sets Activated=true; do not expose an off switch that makes settings disappear. |
RuleOptions.MuslimID |
rule.calendarSources.muslimId |
Signed Integer reference preserved exactly, including -1 (undefined) and 0 (set default). Do not resolve or rewrite it during serialization, even when unused by the active family. |
RuleOptions.MoonID |
rule.calendarSources.moonId |
Signed Integer reference preserved exactly, including -1 (undefined) and 0 (set default). Do not resolve or rewrite it during serialization, even when unused by the active family. |
RuleOptions.SunID |
rule.calendarSources.sunId |
Signed Integer reference preserved exactly, including -1 (undefined) and 0 (set default). Do not resolve or rewrite it during serialization, even when unused by the active family. |
Validity: explicit boundary translation
Mode |
fromYear |
toYear |
Legacy fields |
Always |
None |
None |
Validity=hvAlways; year slots inactive |
Beginning |
2023 |
None |
Validity=hvBeginning; Year1=2023 |
Ending |
None |
2025 |
Validity=hvEnding; Year1=2025 |
FromTo |
2023 |
2025 |
Validity=hvFromTo; Year1=2023; Year2=2025 |
Except |
2023 |
2025 |
Validity=hvExcept; Year1=2023; Year2=2025 |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
RuleOptions.DaysAfterIfWeekday |
rule.weekdays.<weekday>.offsetDays |
Seven separate weekday groups: monday through sunday. Signed day offset; 0 with action=None; legacy 99 for deletion is represented by action=Delete and XML offset=0. |
RuleOptions.ActionTypeIfWeekday |
rule.weekdays.<weekday>.action |
None, Move, Duplicate, Delete. Legacy: offset=0 → None regardless of stale action byte; active 0=Move,1=Duplicate,2=Delete. Delete writes offset=99/action=2. Move/Duplicate must not use zero or the reserved +99 deletion encoding. Emit all seven groups. |
RuleOptions.DaysAfterIfConflict |
rule.conflict.action; rule.conflict.offset.amount |
0 → None; 99 → Delete; otherwise Move with the original signed offset. For None/Delete XML amount=0; reconstruct sentinel only in the legacy adapter. |
RuleOptions.TypeDaysIfConflict |
rule.conflict.offset.units |
0 Days; 1 DaysExcludingHolidays; 2 Weekdays; 3 WeekdaysIncludingSaturday; 4 WeekdaysIncludingSaturdayExcludingHolidays. Legacy -1 maps to LegacyUnspecified only for None/Delete; Move requires specified units. |
RuleOptions.ConflictHolId |
rule.conflict.target.kind; rule.conflict.target.holidayId; rule.conflict.target.setId |
0 → SameSet; positive → Holiday; negative values below -10000 (excluding Low(Integer)) → Set with positive setId. -1 → Unresolved with holidayId=0 and setId=0. Preserve the sentinel, including when action=None; other unsupported negatives are errors. |
Every supported observance flag is explicitly written, including false. The legacy NoneSpecified flag is preserved independently as observance.noneSpecified: real database records can set it together with other flags, and that combination is not rejected or normalized. For older schema-1 XML lacking this element only, the reader derives it from the absence of real observance flags.
Source member / storage |
Proposed XML element(s) |
Value and conversion |
RuleOptions.IgnoreObservance |
observance.ignore |
Boolean. Keep separate from the individual observance settings; preserve the flags even when ignored. |
RuleOptions.ObservanceSettings |
observance.<distinct-name> |
Per-index mapping below. No bitmask, comma-separated code list, or duplicated packed arrays in XML. |
Array index |
XML leaf / disposition |
0 |
observance.noneSpecified (legacy flag preserved independently) |
1 |
[heading] Geographical |
2 |
observance.municipal |
3 |
observance.regional |
4 |
[heading] Categories |
5 |
observance.banks |
6 |
observance.other |
7 |
observance.government |
8 |
observance.linguistic |
9 |
[heading] Times of Day |
10 |
observance.legacyPreviousEve |
11 |
observance.morning |
12 |
observance.afternoon |
13 |
observance.evening |
14 |
[heading] Religious |
15 |
observance.bahai |
16 |
observance.catholic |
17 |
observance.christian |
18 |
observance.coptic |
19 |
observance.buddhist |
20 |
observance.hindu |
21 |
observance.jewish |
22 |
observance.muslim |
23 |
observance.orthodox |
24 |
observance.protestant |
25 |
observance.sikh |
26 |
observance.schools |
27 |
observance.armenian |
28 |
observance.otherReligion |
29 |
observance.webOnly |
30 |
observance.tentative |
31 |
observance.mayDisappear |
32 |
observance.privateSector |
33 |
observance.pontoFacultativo |
Indices 1, 4, 9 and 14 are reserved headings and must be false. Index 10 is preserved as observance.legacyPreviousEve; older XML without this node defaults to false. Indices 32 and 33 remain distinct flags regardless of their split between legacy packed arrays. NoneSpecified is authoritative compatibility data, not an info-only value.
Rule families and subtype tokens
Legacy RuleType |
rule.type |
Subtype / active data |
0 hrFixed |
Fixed |
Gregorian or Julian fixed month/day |
1 hrVariable |
NthWeekday |
Nth weekday of month plus relative day offset |
2 hrEaster |
Easter |
Western/Orthodox and Easter-relative offset |
3 hrArbitrary |
ArbitraryDates |
Explicit dates; occurs-every-year expectation |
4 hrJewish |
Jewish |
Jewish calendar month/day and Adar policy |
5 hrRange |
DateRanges |
Independent ranges with weekdays |
6 hrMuslim |
Muslim |
Muslim month/day and calendar reference |
7 hrChinese |
Chinese |
Lunar=71, SolarTerms=72 |
8 hrEgypt |
Egyptian |
Coptic=80, Ethiopic=81, Egyptian=82, Armenian=83 |
11 hrHindu |
Hindu |
Lunar=110, Solar=111 |
13 hrSikh |
Sikh |
Sikh calendar month/day |
14 hrSun |
Sun |
Solar longitude and day boundary |
16 hrLunisolar |
Lunisolar |
Moon phase relative to solar longitude/month/date |
17 hrPersian |
Persian |
Persian month/day |
18 hrKorean |
Korean |
Lunar=181, SolarTerms=182 |
19 hrSouthAsian |
SouthEastAsian |
FullMoon=191, SpecialRule=192, Calendrical=193 |
20 hrVietnamese |
Vietnamese |
Lunar=201, SolarTerms=202 |
21 hrBurmese |
Burmese |
Lunar=211, NewYearDay=212 |
22 hrTibetan |
Tibetan |
Lunar=221, Solar=222 |
23 hrBhutanese |
Bhutanese |
Lunar=231, Solar=232 |
24 hrMongolian |
Mongolian |
Lunar=241, Solar=242 |
25 hrZoroastrian |
Zoroastrian |
ParsiShenshai=250 supported; Kadmi/Fasli unsupported and rejected. |
The codec covers the 22 active UI rule types. hrOrthodox, hrBahai and hrMoon are not additional active families in the supplied UI; hrConflict=20 aliases hrVietnamese and must not create a second dispatch branch. Zoroastrian Kadmi/Fasli are not implemented calculation choices and are explicitly rejected.
Shared weekday-selection component — resolved from PAS and DFM
The pasted frmWeekdaysBeforeAfter.pas and DFM resolve the composite encoding, calculation semantics, combo-box index meanings and occurrence bounds. The DFM sets spinNumWkDays.Min=1, Max=99 and Value=1. The supplied DataHolidays DFM does not override those limits in its inherited frame controls, and no spinNumWkDays limit assignment was found in the supplied DataHolidays Pascal source.
For each applicable family, write three separate leaves under its uniquely prefixed weekdaySelection group: relation (None, Before, After, OnOrBefore, OnOrAfter), weekday (Monday through Sunday; None when disabled), and occurrence (1 for first, 2 for second, etc.). Do not add a separate inclusivity flag, which would duplicate the meaning of OnOrBefore/OnOrAfter. None uses weekday=None and occurrence=1 as explicit neutral values.
Meaning |
Legacy value |
None |
0 |
Before |
-B |
After |
B |
OnOrBefore |
-(1000+B) |
OnOrAfter |
1000+B |
DFM control / index |
Meaning / XML value |
cbWeekdayAfter 0: day of |
No weekday selection: relation=None, weekday=None, occurrence=1. |
cbWeekdayAfter 1..7 |
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday. |
spinNumWkDays |
Integer occurrence 1..99; initial value 1. |
rgWeekdayBeforeAfter 0 |
before → Before |
rgWeekdayBeforeAfter 1 |
after → After |
rgWeekdayBeforeAfter 2 |
on or before → OnOrBefore |
rgWeekdayBeforeAfter 3 |
on or after → OnOrAfter |
The DFM initially selects relation index 0 (before), but the active relation must always come from WeekdayValue. In particular, SetWeekdayValue(0) selects index 1 (after) while hiding the relation controls. When cbWeekdayAfter is at index 0, the relation and spin controls are irrelevant and GetWeekdayValue returns 0; XML uses the explicit None values above rather than retaining hidden UI state.
B = weekdayNumber + 10 × (occurrence - 1), with weekdayNumber Monday=1 through Sunday=7. Decode a nonzero value using A=Abs(value), B=A mod 1000, weekdayNumber=B mod 10, and occurrence=1+(B div 10). Sign selects Before/After; A>=1000 adds OnOr. The XML validation follows the confirmed editor range: weekdayNumber in 1..7 and occurrence in 1..99, giving B<=987 and A<=1987. The arithmetic encoding can also represent occurrence 100, but this lies outside the current editor range. If migration encounters such a stored value, report the holiday and setting for review; do not clamp it to 99 or silently change its calculated date. Reject nonzero encodings with weekday zero, invalid weekday digits, or out-of-domain magnitudes.
WeekdayAfterToDate makes Before/After strict: a matching weekday advances or retreats by a full week before adding further occurrences. For OnOrAfter it first subtracts one day from the anchor; for OnOrBefore it first adds one day, then performs the same strict search. This makes the anchor count as the first occurrence when it matches. An inclusive second occurrence is still one week later/earlier when the anchor already matches.
Example |
Legacy value |
Interpretation |
Monday after |
1 |
First Monday strictly after the anchor |
Monday on or after |
1001 |
First Monday including a matching anchor |
Second Monday on or after |
1011 |
Second qualifying Monday; one week after a matching Monday anchor |
Fifth Sunday after |
47 |
7 + 10 × (5-1) |
Fifth Sunday on or before |
-1047 |
-(1000 + 47) |
<rule.fixed.weekdaySelection> |
The same decoder/encoder is shared by fixed, Jewish, Muslim, Chinese, Korean, Vietnamese, solar and lunisolar family fields. Their XML leaves remain family-specific to support unambiguous SQL/regex changes. This is distinct from the common rule.weekdays actions that move/duplicate/delete an already calculated occurrence.
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.Day |
rule.fixed.day |
Day number in the selected fixed calendar. |
Rule.fMonth |
rule.fixed.month |
Month 1..12. |
Rule.fOrthodox |
rule.fixed.calendar |
false → Gregorian; true → Julian. Preserve the existing conversion, not a fixed 13-day offset. |
Rule.WeekdayAfter |
rule.fixed.weekdaySelection.relation; rule.fixed.weekdaySelection.weekday; rule.fixed.weekdaySelection.occurrence |
Use the resolved shared weekday codec. Legacy 0 maps to relation=None, weekday=None, occurrence=1. |
Rule.ImportationYear |
rule.fixed.importYear |
Preserve the integer provenance value used by the form; 0 remains 0. Meaning of nonzero values should be documented from the import path. |
Rule.fDaysAfterIfConflict |
no independent XML leaf; common weekday/conflict options |
Historical branch-local setting consumed by ConvertToNewRecurrenceOptions. Load through the legacy conversion before export. Current RuleOptions is the single XML authority; keep compatibility output semantics and regression-test old records. |
Rule.fDaysAfterIfSaturday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local setting consumed by ConvertToNewRecurrenceOptions. Load through the legacy conversion before export. Current RuleOptions is the single XML authority; keep compatibility output semantics and regression-test old records. |
Rule.fDaysAfterIfSunday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local setting consumed by ConvertToNewRecurrenceOptions. Load through the legacy conversion before export. Current RuleOptions is the single XML authority; keep compatibility output semantics and regression-test old records. |
Rule.fConflictHolId |
no independent XML leaf; common weekday/conflict options |
Historical branch-local setting consumed by ConvertToNewRecurrenceOptions. Load through the legacy conversion before export. Current RuleOptions is the single XML authority; keep compatibility output semantics and regression-test old records. |
Rule.fDupIfSat |
no independent XML leaf; common weekday/conflict options |
Historical branch-local setting consumed by ConvertToNewRecurrenceOptions. Load through the legacy conversion before export. Current RuleOptions is the single XML authority; keep compatibility output semantics and regression-test old records. |
Rule.fDupIfSun |
no independent XML leaf; common weekday/conflict options |
Historical branch-local setting consumed by ConvertToNewRecurrenceOptions. Load through the legacy conversion before export. Current RuleOptions is the single XML authority; keep compatibility output semantics and regression-test old records. |
Rule.fDaysAfterIfMonday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local setting consumed by ConvertToNewRecurrenceOptions. Load through the legacy conversion before export. Current RuleOptions is the single XML authority; keep compatibility output semantics and regression-test old records. |
Rule.fDupIfMon |
no independent XML leaf; common weekday/conflict options |
Historical branch-local setting consumed by ConvertToNewRecurrenceOptions. Load through the legacy conversion before export. Current RuleOptions is the single XML authority; keep compatibility output semantics and regression-test old records. |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.NthDay |
rule.nthWeekday.ordinal |
First=1, Second=2, Third=3, Fourth=4, Fifth=5, Penultimate=-1, Last=0. Stable symbolic tokens remove the special negative/zero ordinal encoding. |
Rule.Weekday |
rule.nthWeekday.day |
Monday..Sunday ↔ 1..7. |
Rule.vMonth |
rule.nthWeekday.month |
Month 1..12. |
Rule.DaysAfter |
rule.nthWeekday.relativeOffsetDays |
Signed calendar-day offset relative to the selected weekday. Distinct from the common rule.offset.amount; preserve both independently. |
Rule.vDaysAfterIfConflict |
no independent XML leaf; rule.conflict.* |
Historical conflict fields; normalize using the existing options conversion. |
Rule.vConflictHolId |
no independent XML leaf; rule.conflict.* |
Historical conflict fields; normalize using the existing options conversion. |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.DaysAfterEaster |
rule.easter.offsetDays |
Signed calendar days before/after Easter; weeks in the UI are already converted to days. |
Rule.isOrthodox |
rule.easter.calendar |
Western or Orthodox; false/true respectively. |
Rule.OmitIfBothEastersSame |
rule.easter.omitWhenSameDate |
Boolean; preserve even when false. |
Rule.eDaysAfterIfConflict |
no independent XML leaf; rule.conflict.* |
Historical conflict fields; normalize through the existing reader. |
Rule.eConflictHolId |
no independent XML leaf; rule.conflict.* |
Historical conflict fields; normalize through the existing reader. |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.jMonth |
rule.jewish.month |
Existing Jewish month number, not a Gregorian month. Validate against the calendar helper. |
Rule.jDay |
rule.jewish.day |
Existing Jewish day number. |
Rule.MoveAdarOnLeapYears |
rule.jewish.adarLeapYearPolicy |
AdarI=0, AdarII=1, Omit=2, confirmed from DFM order. |
Rule.jWeekdayAfter |
rule.jewish.weekdaySelection.relation; rule.jewish.weekdaySelection.weekday; rule.jewish.weekdaySelection.occurrence |
Use the resolved shared weekday codec documented above. |
Rule.MoveSatToSun |
no independent XML leaf; rule.weekdays.* |
Historical flag: ConvertToNewRecurrenceOptions translates it. Do not duplicate current weekday policy. |
Rule.iMonth |
rule.muslim.month |
Muslim month number. |
Rule.iDay |
rule.muslim.day |
Muslim day number. |
Rule.iWeekdayAfter |
rule.muslim.weekdaySelection.relation; rule.muslim.weekdaySelection.weekday; rule.muslim.weekdaySelection.occurrence |
Use the resolved shared weekday codec documented above. |
Rule.iEntireMonth |
rule.muslim.entireMonth |
Stored boolean but not visibly used in the supplied current calculator/UI path. Preserve provisionally; investigate non-default records before deciding it is obsolete. Do not equate it with HolDuration without evidence. |
Rule.iDaysAfterIfFriday |
no independent XML leaf; rule.weekdays.* |
Historical weekday setting; normalize through ConvertToNewRecurrenceOptions. |
Rule.iDaysAfterIfSaturday |
no independent XML leaf; rule.weekdays.* |
Historical weekday setting; normalize through ConvertToNewRecurrenceOptions. |
Rule.iDaysAfterIfSunday |
no independent XML leaf; rule.weekdays.* |
Historical weekday setting; normalize through ConvertToNewRecurrenceOptions. |
Rule.iDupIfFri |
no independent XML leaf; rule.weekdays.* |
Historical weekday setting; normalize through ConvertToNewRecurrenceOptions. |
Rule.iDupifSat |
no independent XML leaf; rule.weekdays.* |
Historical weekday setting; normalize through ConvertToNewRecurrenceOptions. |
Rule.iDupifSun |
no independent XML leaf; rule.weekdays.* |
Historical weekday setting; normalize through ConvertToNewRecurrenceOptions. |
Chinese, Korean and Vietnamese rules
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.chChineseType |
rule.chinese.mode |
Lunar or SolarTerms, using the exact subtype constants in the family table. |
Rule.chlMonth |
rule.chinese.lunar.month |
Lunar subtype only; calendar month number. |
Rule.chlDay |
rule.chinese.lunar.day |
Lunar subtype only; calendar day number. |
Rule.chlLeapMonth |
rule.chinese.lunar.leapMonth |
Lunar subtype only; preserve boolean. |
Rule.chsNthTerm |
rule.chinese.solarTerm.number |
SolarTerms subtype only; positive term ordinal. |
Rule.chsMinorOrMajor |
rule.chinese.solarTerm.kind |
Minor=0; Major=1. SolarTerms subtype only. |
Rule.chWeekdayAfter |
rule.chinese.weekdaySelection.relation; rule.chinese.weekdaySelection.weekday; rule.chinese.weekdaySelection.occurrence |
Use the resolved shared weekday codec; applies to the active subtype. |
Rule.chDaysAfterIfFriday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.chDaysAfterIfSaturday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.chDaysAfterIfSunday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.chConflictHolId |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.chDaysAfterIfConflict |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.chTypeDaysIfConflict |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.chDupIfFri |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.chDupifSat |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.chDupifSun |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.koKoreanType |
rule.korean.mode |
Lunar or SolarTerms, using the exact subtype constants in the family table. |
Rule.kolMonth |
rule.korean.lunar.month |
Lunar subtype only; calendar month number. |
Rule.kolDay |
rule.korean.lunar.day |
Lunar subtype only; calendar day number. |
Rule.kolLeapMonth |
rule.korean.lunar.leapMonth |
Lunar subtype only; preserve boolean. |
Rule.kosNthTerm |
rule.korean.solarTerm.number |
SolarTerms subtype only; positive term ordinal. |
Rule.kosMinorOrMajor |
rule.korean.solarTerm.kind |
Minor=0; Major=1. SolarTerms subtype only. |
Rule.koWeekdayAfter |
rule.korean.weekdaySelection.relation; rule.korean.weekdaySelection.weekday; rule.korean.weekdaySelection.occurrence |
Use the resolved shared weekday codec; applies to the active subtype. |
Rule.koDaysAfterIfFriday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.koDaysAfterIfSaturday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.koDaysAfterIfSunday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.koConflictHolId |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.koDaysAfterIfConflict |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.koTypeDaysIfConflict |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.koDupIfFri |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.koDupifSat |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.koDupifSun |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.vieVietnameseType |
rule.vietnamese.mode |
Lunar or SolarTerms, using the exact subtype constants in the family table. |
Rule.vielMonth |
rule.vietnamese.lunar.month |
Lunar subtype only; calendar month number. |
Rule.vielDay |
rule.vietnamese.lunar.day |
Lunar subtype only; calendar day number. |
Rule.vielLeapMonth |
rule.vietnamese.lunar.leapMonth |
Lunar subtype only; preserve boolean. |
Rule.viesNthTerm |
rule.vietnamese.solarTerm.number |
SolarTerms subtype only; positive term ordinal. |
Rule.viesMinorOrMajor |
rule.vietnamese.solarTerm.kind |
Minor=0; Major=1. SolarTerms subtype only. |
Rule.vieWeekdayAfter |
rule.vietnamese.weekdaySelection.relation; rule.vietnamese.weekdaySelection.weekday; rule.vietnamese.weekdaySelection.occurrence |
Use the resolved shared weekday codec; applies to the active subtype. |
Rule.vieDaysAfterIfFriday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.vieDaysAfterIfSaturday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.vieDaysAfterIfSunday |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.vieConflictHolId |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.vieDaysAfterIfConflict |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.vieTypeDaysIfConflict |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.vieDupIfFri |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.vieDupifSat |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Rule.vieDupifSun |
no independent XML leaf; common weekday/conflict options |
Historical branch-local value; load and normalize before XML export. Avoid a second editable version of the same policy. |
Egyptian-family and Zoroastrian rules
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.egEgyptianType |
rule.egyptian.calendar |
Coptic, Ethiopic, Egyptian, Armenian ↔ 80,81,82,83. |
Rule.egMonth |
rule.egyptian.month |
Month 1..13, preserving the editor range. |
Rule.egDay |
rule.egyptian.day |
Day 1..30. Preserve month-13 combinations even when the existing calculator returns no date; do not add calendar occurrence validation to serialization. |
Rule.zoZoroastrianType |
rule.zoroastrian.calendar |
ParsiShenshai maps to hrParsi=250. Kadmi/Fasli are not supported by this codec and are rejected. |
Rule.zoMonth |
rule.zoroastrian.month |
Month 1..13. |
Rule.zoDay |
rule.zoroastrian.day |
Day 1..30; occurrence validity remains the calculator’s responsibility. |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.hiHinduType |
rule.hindu.mode |
Lunar=110 or Solar=111. Write only the active branch. |
Rule.hilMonth |
rule.hindu.lunar.month |
Existing lunar month number; preserve calendar numbering. |
Rule.hilDay |
rule.hindu.lunar.tithi |
Existing tithi number (DFM has 30 choices). |
Rule.hilLeapMonthIfExists |
rule.hindu.lunar.preferLeapMonth |
Boolean; preserve current meaning in the calendar helper. |
Rule.hilLeapDay |
rule.hindu.lunar.preferLeapDay |
Boolean; preserve current meaning. |
Rule.hilJunkField |
not serialized |
Reserved/junk field; not copied by the current pack/unpack assignments. Initialize to zero. |
Rule.hilMoveByIfExpunged |
rule.hindu.lunar.expungedOffsetDays |
Signed integer; shown in StringFormula. Preserve and verify the calendar helper behaviour; do not discard because the shown UI does not assign it. |
Rule.hilTimeInterval |
rule.hindu.lunar.time.mode |
Stable tokens for all 17 choices listed in the timing table below. |
Rule.hilTime1 |
rule.hindu.lunar.time.muhurta.from / rule.hindu.lunar.time.ghatikas |
For Muhurtas use the lower bound; for a GhatikasCovering* mode use the ghatika amount. Other modes have no active numeric parameter; normalize unused slots. |
Rule.hilTime2 |
rule.hindu.lunar.time.muhurta.to |
Upper muhurta bound in Muhurtas mode only. Other modes do not emit this leaf. |
Rule.hisMonth |
rule.hindu.solar.month |
Existing solar month number, both solar submodes. |
Rule.hisDay |
rule.hindu.solar.day |
DayNumber solar submode only. |
Rule.hisUseNakshatra |
rule.hindu.solar.selection |
false → DayNumber; true → Nakshatra. This selects the active solar substructure. |
Rule.hisNakshatra1 |
rule.hindu.solar.nakshatra.number |
One nakshatra, numbered 1..27 in XML; legacy zero-based value = XML number - 1. The DFM lists 27 choices starting at index 0; the pasted helper uses Nakshatra1 × 360/27 degrees. No interval is calculated. |
Rule.hisNakshatra2 |
not serialized [pending compatibility audit] |
The helper accepts Nakshatra2 but does not read it. No interval-end leaf is serialized. The codec rejects a nonzero active hisNakshatra2 pending a compatibility audit of other consumers, including XE3. Fresh XML reads leave the unused slot at zero. |
Rule.hisNakshatraCalcTime |
rule.hindu.solar.nakshatra.calculationTime |
Midnight=0, Sunrise=1, Noon=2, Midday=3. Keep Noon and Midday distinct exactly as the DFM does. |
Rule.hisNakshatraLeapIfOccurs |
rule.hindu.solar.nakshatra.preferSecondOccurrence |
Boolean, Nakshatra submode only. Passed as UseSecondNakshatra. true prefers a later occurrence of the same Nakshatra1 when the helper finds it in the requested solar month under its existing search conditions; otherwise retain the first. It neither selects Nakshatra2 nor adds a second holiday date. |
Rule.hisMoveByIfExpunged |
rule.hindu.solar.expungedOffsetDays |
Signed integer; preserve pending calendar-helper verification. |
Hindu solar nakshatra — behaviour confirmed from helper
The supplied helper computes the solar-month ingress after 1 January for longitude (hisMonth - 1) × 30 degrees, then finds the next lunar longitude crossing at Nakshatra1 × 360/27 degrees. Nakshatra2 is not read anywhere in the function body. The XML therefore describes one nakshatra number, not a from/to interval.
The first candidate is converted to a date by CriticalTimeType. If UseSecondNakshatra is true and (dteHoliday1 - dateCurSolarMonth) < 10, the helper searches again after dteHoliday1 + 2 for the SAME Nakshatra1 longitude. It accepts the later candidate only if hisMonthOf(Trunc(dteApxHoliday)) still equals hisMonth. The second candidate is then converted using the same critical-time calculation and overwrites Result[Low(ThiDateOccurences)]. Otherwise the first candidate remains. Preserve this exact gate and pre-conversion month check; preferSecondOccurrence is a preference, not a requirement that a second candidate exist.
Critical-time tokens remain Midnight=0, Sunrise=1, Noon=2 and Midday=3. Preserve the supplied formulas. In particular, case 3 adds Sunset(date) - Sunrise(date), the daylight duration, to the event time before truncation. That expression is not the midpoint of sunrise and sunset. Whether this matches the intended Midday definition is a separate calculation review; serialization must not quietly replace it with another formula.
The three-element occurrence result has an established purpose: as clarified by the user, dates belonging to Gregorian year Y can come from calculating the corresponding holiday for Y-1, Y or Y+1. Retain that shared return structure and the caller loop. The narrower initialization check concerns this particular nakshatra helper, which assigns only Result[Low(ThiDateOccurences)] and overwrites that same slot if it selects the later occurrence. The other result slots are not explicitly initialized in the shown function. AddMultiDate accepts only positive dates by default, so unused entries should be zero. For the fixed numeric result array implied by the shown code, the recommended correction is to insert Result := Default(ThiDateOccurences); as the first statement of HinduSolarNakshatraInGregorianYear, before dteHoliday1 := 0. Keep both assignments to Result[Low(ThiDateOccurences)] and the three-slot caller loop unchanged. This initializes unused entries without altering how the selected date is calculated. Compile and check first- and second-occurrence cases, confirming unused slots are zero. The correction is recommended, not recorded as applied or tested in Delphi; it remains separate from XML migration.
hilTimeInterval |
Stable XML token |
0 |
Sunrise |
1 |
Arunodaya |
2 |
Pratah |
3 |
Purvahna |
4 |
Madhyahna |
5 |
Aparahna |
6 |
Sayahna |
7 |
Pradosha |
8 |
ExtendedPradosha |
9 |
Nisitha |
10 |
Purvaviddha |
11 |
Paraviddha |
12 |
Muhurtas |
13 |
GhatikasCoveringSunrise |
14 |
GhatikasCoveringMidday |
15 |
GhatikasCoveringSunset |
16 |
GhatikasCoveringMidnight |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.buBurmeseType |
rule.burmese.mode |
Lunar=211 or NewYearDay=212. |
Rule.burMonth |
rule.burmese.lunar.month |
Calendar month number; Lunar only. |
Rule.burDay |
rule.burmese.lunar.day |
Calendar day number; Lunar only. |
Rule.burLeapMonthIfExists |
rule.burmese.lunar.preferLeapMonth |
Stored Boolean preserved even though the control is hidden/disabled and the current calculator passes literal true. Serialization does not alter that calculation. |
Rule.BurNewYearDay |
rule.burmese.newYearDay |
Akyo=0, Akya=1, FirstAkyat=2, SecondAkyat=3, Atat=4; NewYearDay only. |
Tibetan, Bhutanese and Mongolian rules
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.tibTibetanType |
rule.tibetan.mode |
Lunar or Solar; subtype constants listed in the family table. |
Rule.tibMonth |
rule.tibetan.lunar.month |
Calendar month number. Emit only the active subtype. |
Rule.tibDay |
rule.tibetan.lunar.day |
Calendar day number. Emit only the active subtype. |
Rule.tibLeapMonthIfExists |
rule.tibetan.lunar.preferLeapMonth |
Boolean. Emit only the active subtype. |
Rule.tibLeapDayIfExists |
rule.tibetan.lunar.preferLeapDay |
Boolean. Emit only the active subtype. |
Rule.tibMansion |
rule.tibetan.solar.mansion.number |
Integer 0..26 in the existing editor numbering; Solar subtype only. |
Rule.tibMansionFraction |
rule.tibetan.solar.mansion.fraction |
Integer 0..59 in the stored editor units; no conversion to degrees. Solar subtype only. |
Rule.bhuBhutaneseType |
rule.bhutanese.mode |
Lunar or Solar; subtype constants listed in the family table. |
Rule.bhuMonth |
rule.bhutanese.lunar.month |
Calendar month number. Emit only the active subtype. |
Rule.bhuDay |
rule.bhutanese.lunar.day |
Calendar day number. Emit only the active subtype. |
Rule.bhuLeapMonthIfExists |
rule.bhutanese.lunar.preferLeapMonth |
Boolean. Emit only the active subtype. |
Rule.bhuLeapDayIfExists |
rule.bhutanese.lunar.preferLeapDay |
Boolean. Emit only the active subtype. |
Rule.bhuMansion |
rule.bhutanese.solar.mansion.number |
Integer 0..26 in the existing editor numbering; Solar subtype only. |
Rule.bhuMansionFraction |
rule.bhutanese.solar.mansion.fraction |
Integer 0..59 in the stored editor units; no conversion to degrees. Solar subtype only. |
Rule.mogMongolianType |
rule.mongolian.mode |
Lunar or Solar; subtype constants listed in the family table. |
Rule.mogMonth |
rule.mongolian.lunar.month |
Calendar month number. Emit only the active subtype. |
Rule.mogDay |
rule.mongolian.lunar.day |
Calendar day number. Emit only the active subtype. |
Rule.mogLeapMonthIfExists |
rule.mongolian.lunar.preferLeapMonth |
Boolean. Emit only the active subtype. |
Rule.mogLeapDayIfExists |
rule.mongolian.lunar.preferLeapDay |
Boolean. Emit only the active subtype. |
Rule.mogMansion |
rule.mongolian.solar.mansion.number |
Integer 0..26 in the existing editor numbering; Solar subtype only. |
Rule.mogMansionFraction |
rule.mongolian.solar.mansion.fraction |
Integer 0..59 in the stored editor units; no conversion to degrees. Solar subtype only. |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.siMonth |
rule.sikh.month |
Month 1..12. |
Rule.siDay |
rule.sikh.day |
Day 1..31. |
Rule.peMonth |
rule.persian.month |
Month 1..12. |
Rule.peDay |
rule.persian.day |
Day 1..32, matching the stored editor domain. |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.suType |
rule.solar.event |
SolarLongitude ↔ 1 (the only type assigned by this form). Inventory other legacy values before migration; do not invent additional events. |
Rule.suLongIndex |
rule.solar.longitude.selection |
0..11 → Preset000Degrees, Preset030Degrees, ... Preset330Degrees; 12 → Custom. |
Rule.suLongitudeTimesThousand |
rule.solar.longitude.degrees |
Exact fixed-point degrees: 90.000 ↔ 90000. When a preset is selected, require consistency with its degrees; do not let the UI selector and value disagree. |
Rule.suDayStartsAtSunrise |
rule.solar.dayBoundary |
Midnight=0, Sunrise=1, PreviousSunset=2, Poya=3, Sunset=4, Pradosha=5. Names mirror distinct existing options; calculation remains in the existing engine. |
Rule.suWeekdayAfter |
rule.solar.weekdaySelection.relation; rule.solar.weekdaySelection.weekday; rule.solar.weekdaySelection.occurrence |
Use the resolved shared weekday codec documented above. |
Rule.suSidereal |
rule.solar.zodiac |
Tropical=false; Sidereal=true. |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.luniWeekdayAfter |
rule.lunisolar.weekdaySelection.relation; rule.lunisolar.weekdaySelection.weekday; rule.lunisolar.weekdaySelection.occurrence |
Use the resolved shared weekday codec documented above. |
Rule.luniNthPhase |
rule.lunisolar.phase.occurrence |
Integer 1..99, explicitly preserved even when ignored by ClosestToDate. |
Rule.luniMoonLongIndex |
rule.lunisolar.phase.selection |
0..7 → Preset000Degrees, Preset045Degrees, ... Preset315Degrees; 8 → Custom. |
Rule.luniMoonLongitudeTimesThousand |
rule.lunisolar.phase.degrees |
Exact fixed-point phase angle /1000; consistency with a preset required. |
Rule.luniReferenceType |
rule.lunisolar.reference.kind |
Ten stable tokens in the following table; choose the corresponding active reference data. |
Rule.luniFullCycles |
rule.lunisolar.fullCycles |
Boolean, explicit even where it does not affect the selected reference mode; preserve current calculator behaviour. |
Rule.luniSunLongIndex |
rule.lunisolar.reference.solarLongitude.selection |
Solar-reference modes only: 0..11 are 30-degree presets; 12 is Custom. |
Rule.luniSunLongitudeTimesThousand |
rule.lunisolar.reference.solarLongitude.degrees |
Solar-reference modes only. Exact fixed-point degrees /1000. |
Rule.luniDateMonth |
rule.lunisolar.reference.month |
Month/date reference modes only. Gregorian month number. |
Rule.luniDateDay |
rule.lunisolar.reference.day |
Date reference modes only. Gregorian day number. |
Rule.JUNK_luniDayStartsAtSunrise |
not serialized |
Obsolete storage slot; not copied by current pack/unpack. Initialize to zero. |
Rule.luniDayStartsAtSunrise |
rule.lunisolar.dayBoundary |
Midnight=0, Sunrise=1, PreviousSunset=2, Poya=3, Sunset=4, Pradosha=5. Preserve the existing lunisolar calculation, which is not assumed identical to the solar code. |
Rule.luniSidereal |
rule.lunisolar.zodiac |
Tropical=false; Sidereal=true. Preserve explicit setting where stored; do not silently derive it from visibility of a UI control. |
luniReferenceType |
XML token |
Active reference values |
0 |
BeforeSolarLongitude |
solar longitude selection + degrees |
1 |
AfterSolarLongitude |
solar longitude selection + degrees |
2 |
InOrBeforeMonth |
month |
3 |
InMonth |
month |
4 |
InOrAfterMonth |
month |
5 |
BeforeDate |
month + day |
6 |
OnOrBeforeDate |
month + day |
7 |
OnOrAfterDate |
month + day |
8 |
AfterDate |
month + day |
9 |
ClosestToDate |
month + day |
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.saAsianType |
rule.southEastAsian.mode |
FullMoon=191; SpecialRule=192; Calendrical=193. |
Rule.saEra |
rule.southEastAsian.era |
Confirmed mapping: _cs=1 Chulasakarat; _ms=2 Mahasakarat; _bs=3 Buddhasakarat; _as=4 Anchansakarat; _be=5 BurmeseEra; _th=6 ThaiEra. Form stores ItemIndex+1. Use an explicit fixed conversion table; the pasted display labels establish the correspondence but are not parsed dynamically. |
Rule.samNthMoon |
rule.southEastAsian.fullMoon.number |
FullMoon only; integer 1..12. |
Rule.sasRuleNumber |
rule.southEastAsian.specialRule |
NewYear=1, MakhaBuchha=2, VisakhaBuchha=3, KhaoPansa=4, confirmed by both the calculator switch and pasted SpecialHolidays assignments. Use these stable tokens, not runtime captions. SpecialRule submode only. |
Rule.sacMonth |
rule.southEastAsian.calendar.month |
Calendrical only; calendar month number. |
Rule.sacDay |
rule.southEastAsian.calendar.day |
Calendrical only; calendar day number. |
Rule.sacLeap |
rule.southEastAsian.calendar.leap |
Calendrical only. Preserve: the calculator passes this value although the shown UI does not assign it. |
South-East Asian era and special-rule tokens — confirmed
Legacy era |
Pascal constant |
Proposed XML token |
1 |
_cs |
Chulasakarat |
2 |
_ms |
Mahasakarat |
3 |
_bs |
Buddhasakarat |
4 |
_as |
Anchansakarat |
5 |
_be |
BurmeseEra |
6 |
_th |
ThaiEra |
Legacy special rule |
Proposed XML token |
1 |
NewYear |
2 |
MakhaBuchha |
3 |
VisakhaBuchha |
4 |
KhaoPansa |
The commented-out index-0 Undefined Era / Undefined Holiday assignments do not establish active XML choices. The documented domain is eras 1..6 and special rules 1..4; report other stored values rather than guessing a fallback. Keep these mappings explicit in code, preferably referring to the supplied era constants. XML uses fixed tokens even if display captions later change. The specialRule leaf is present only for the SpecialRule subtype.
Arbitrary dates and date ranges
Source member / storage |
Proposed XML element(s) |
Value and conversion |
Rule.StartDate |
not serialized |
Unused legacy slot of the hrArbitrary variant; current PackRule/UnpackRule do not transfer it. Actual dates are in ArbitraryDates. |
Rule.EndDate |
not serialized |
Unused legacy slot of the hrArbitrary variant; do not reinterpret as the last arbitrary date. |
Rule.ArbitraryEveryYear |
rule.arbitraryDates.occursEveryYear |
Annual expectation, not repetition of a stored date. In Olivier’s filtered UI, true keeps the rule visible even without dates in or after the filter year. Preserve the legacy EVERY_YEAR / NOT_EVERY_YEAR conversion. |
Rule.RangeEveryYear |
rule.ranges.occursEveryYear |
Annual expectation, not an instruction to repeat a range. Preserve EVERY_YEAR / NOT_EVERY_YEAR conversion. |
ArbitraryDates[i] (hrArbitrary) |
rule.arbitraryDates.date |
Repeated YYYY-MM-DD element under rule.arbitraryDates.dates. No year markers or serial integers in XML. Preserve validated chronological ordering; reject overcapacity instead of truncating. |
DateRanges[i].StartDate |
rule.range.startDate |
YYYY-MM-DD under a repeated rule.range item in rule.ranges.items. |
DateRanges[i].Duration |
rule.range.durationDays |
Positive integer, inclusive end = start + duration - 1. Distinct from common holiday duration. Preserve the current Word bound. |
DateRanges[i].Weekdays[d] |
rule.range.weekdays.<weekday>.included |
Seven explicit booleans, Monday..Sunday mapped to 1..7. The range duration spans calendar days; these flags exclude dates within that span. |
Rule.MaxBuffer |
not serialized |
Variant-record allocation padding, not holiday data. |
Exceptions: replace a year or omit it
For supported recurring families other than ArbitraryDates/DateRanges, rule.exceptions contains repeated rule.exception entries. Each entry has rule.exception.year, rule.exception.action (ReplaceDates or OmitYear), and a rule.exception.dates container of repeated rule.exception.date elements. ReplaceDates requires at least one actual date in the named year; OmitYear requires an empty dates container. A year cannot have contradictory entries.
The legacy adapter rebuilds the chronological stream of year markers followed by that year’s replacement dates. OmitYear is a year marker with no following dates before the next marker/end. Reject malformed streams during migration; do not silently associate an orphan date with the wrong year. Check combined marker+date capacity against the 127-word array and its terminating-zero requirements; review existing consumers that access the following slot before choosing the maximum accepted count.
<rule.exceptions> |
Replacement dates replace the normal yearly result; they do not supplement it. Preserve the existing engine’s ordering of exceptions, validity, frequency, offsets and duration. Serialization is not an opportunity to reorder calculation stages based on XML element order or a simplified reading of documentation.
Packed-only fields and historical compatibility
Packed-only field(s) |
Disposition |
THolidayRule_packed.RuleType |
Maps to rule.type; not duplicated inside the active subtype. |
DaysAfterJewishDate |
Old-reader compatibility override becomes common rule.offset.amount with units=Days and duration=1; current PackRule writes 0 to this obsolete slot. |
DaysAfterMuslimDate |
Old-reader compatibility override becomes common offset with units=Days; current PackRule writes 0. |
iMoveFriToSat |
Legacy packed-only flag; current unpack does not copy it and current pack writes false. Check older conversion requirements if historical records contain it. |
iExtraDays |
Old-reader compatibility override becomes HolDuration=iExtraDays+1 when positive; current pack writes 0. |
TRuleOptions_packed.ObservanceSettings2 |
Storage extension for logical observance indexes beyond 31. No separate XML element. |
ByteBool / LongInt / alignment / padding |
Binary format details remain in legacy adapters only. Do not change packed declarations or field sizes as part of this feature. |
Filter Year: one shared, explicit filtering operation
Filter Year is a caller-supplied export/synchronization policy, not an intrinsic holiday property. Normal saving and migration preserve all history. When producing a filtered updater or synchronization destination, filter a temporary copy, then serialize that same copy into both XML and legacy fields. Do not prune the master worldwide database merely because an export uses a cutoff. Do not filter the legacy fields and leave unfiltered XML alongside them.
Data |
Filter Year Y policy |
Source evidence / boundary |
Name variations |
Remove complete blocks whose year condition cannot match any year >=Y. Keep all other blocks in their original order. |
StringNameVariations already checks explicit year/range upper ends and preserves + forms. Shared extraction/filtering helper proposed. |
Abbreviation variations |
Apply the same condition analysis as names, retaining whole blocks and their exact text. |
Proposed shared policy; verify existing synchronization consumers. |
Arbitrary dates |
Remove stored dates earlier than 1 January Y. |
SaveArbitraryDatesToTable(minYear) tests the actual date, not merely whether its rule has future occurrences. |
Date ranges |
Remove only a range whose inclusive end (start + duration - 1) is earlier than 1 January Y. |
SaveDateRangesToTable(minYear). Keep a crossing range whole, including its original start and weekday mask; do not clip or shift it. |
Replacement exceptions |
Remove complete exception-year entries earlier than Y, including their replacement dates. |
Old writer filters year markers and dates; structured XML permits removal as a single unit. |
Omitted-year exceptions |
Remove omitted-year entries earlier than Y. |
An omitted-year entry is still historical exception data even though it contains no dates. |
Validity, frequency and reference year |
Preserve. Do not rewrite Beginning to Y or change the frequency anchor. |
These are recurrence settings, not historical lists. |
Occurs-every-year expectations |
Preserve even when pruning leaves a list empty. |
Changing the expectation would change the meaning of missing dates. |
OneTimeDates |
Continue the existing separate updater/synchronization filtering. |
Outside this recurring-holiday XML structure. |
For arbitrary-date rules with duration/offset extending into the retained year, the current legacy pruning uses the stored base date, not the final expanded occurrences. Preserve that established policy initially and explicitly review such cross-boundary cases if changing it; do not quietly apply the date-range rule to arbitrary dates. Other interactions with name-year conditions must use the established final-occurrence-year semantics.
Condition, Filter Year 2026 |
Action |
2024 or 2024_DUP |
Remove |
2021..2025 or 2025- |
Remove |
2025..2028 |
Retain unchanged |
2023+, 2023+L5, 2023+C |
Retain unchanged |
DUP, MOV, BASE, DEFAULT, ALWAYS, DAY2 |
Retain unchanged |
Unrecognized condition |
Report for review; never delete on a guessed interpretation |
Informational elements and search behaviour
The implemented info section writes info.holiday.id, info.holiday.setId, info.holiday.modifiedDate, info.sort.month and info.sort.day, each with info="true". These are not restored from XML: the destination supplies ID, CountryID, DateModified, RN1 and RN2. Additional human-readable descriptions and resolved names remain possible future additions, not current required nodes.
A raw SQL/regex change does not immediately regenerate explanatory text or modification metadata. The generation/preparation save refreshes it from validated authoritative settings. Search editable leaves when exact current truth matters. Dates previews, if ever included, must identify the requested year and calculation context and remain info-only; they are not part of version 1 input.
Shared read/save service and generation integration
Keep serialization separate from database posting. Implemented helpers are SaveHolidayToXML, ReadHolidayFromXML, HolidayXMLIsBlank and ReadHolidayFromStorage; THoliday wrappers call the codec. The storage helper selects NULL/blank legacy input versus nonblank XML and loads a temporary object. Shared paired database persistence, explicit Filter Year application and integration into all callers remain to be completed.
Refactor the form-dependent persistence: scalar fields, all BLOBs, RN1/RN2 derivation, timestamp rules, country/set timestamps, change logs and broadcasts must be callable without a form. A save must not depend on a label caption to determine whether a rule changed. Compare logical data, distinguishing storage-only conversion, an unchanged resave, a true change, and intentional historical pruning. Use semantic comparisons rather than raw record memory or XML whitespace.
For generation: load each stored rule through the shared loader; if the option is enabled, validate and persist both representations before filtering it out for having no calculated dates. Perform at most the intended save per record, not per occurrence/year. A successful preparation state requires all packaged records to have been processed successfully; an XML error must identify the record and prevent treating a partially synchronized database as ready for the XE3 package. Transaction/chunk boundaries should fit existing NexusDB generation behaviour.
For cached datasets, Post alone is not the whole persistence boundary; include the actual apply/update and commit path in the atomic save design. Keep ID assignment after posting in mind: the current AfterPost assigns a new ID when needed, so informational identity must be refreshed after final identity exists. Never overwrite retained operational columns from a partial THoliday instance.
Area |
Required change / evidence |
DataHolidays.SaveHoliday |
Extract full save/housekeeping service; preserve normal GUI behaviour. |
GetHolidayFromTable and other readers |
Route authoritative settings through the XML-aware loader; initialize complete objects; explicitly load ProvinceID and required context. |
mnuRemoveNameVariationsClick |
Currently writes LocalName directly. Change through the shared full load/edit/save service. |
mnuRemoveObservanceClick |
Currently saves only the legacy rule. Update both representations. |
New / duplicate / copy / import |
Assign destination identity correctly, remap external references when needed, and regenerate XML. |
Holidays synchronization |
Filter the selected destination copy and save both formats; handle ancestry/ignored/kept policy explicitly. |
Diary generation / worldwide packaging |
Use the existing every-rule traversal for optional conversion plus resynchronization; expose a reliable success/error result before packaging. |
NexusDB schema and DFM |
Add the XML memo to the real schema, persistent fields, SELECT and both update/insert SQL lists; audit schema propagation to updater destinations. |
Other database maintenance commands |
Search the full project for direct HolRules writes, field assignments, copied BLOBs, SQL modifications and Save*ToTable calls. Supplied files cannot establish full repository coverage. |
HOLCALC / STAND_ALONE builds |
Check dependencies and conditional compilation. Do not impose new XML/UI dependencies on builds that must continue using the legacy reader. |
XE3 validation |
After XV4 writes legacy output, load/calculate using the actual XE3 build or equivalent verified reader. XE3 source/build was not supplied here. |
Validation: completed round trips and remaining checks
Error messages should include NoSite/ID_Holiday, rule type, full dotted XML element path, offending value and the reason. Parse/validate into a temporary object first. Missing mandatory nodes never inherit fields from a previously reused THoliday. Keep the original row unchanged after a failure. Reference checks should distinguish a broken ID from a set-default reference; exact database scope must be preserved.
Check |
Required evidence |
Source coverage |
Every THoliday public data member, THolidayRule member and TRuleOptions member has a mapping or explicit disposition. Packed-only additions are accounted for separately. |
Legacy → XML → new object |
Compare semantic fields, active subtype, ordered text blocks, arrays, all observances and references; allow only documented neutralization of unused storage. |
XML → legacy → legacy reader |
Compare with the authoritative object after normalization; verify that historical compatibility overrides cannot reactivate stale values. |
Representative calculations |
Cover each active family/subtype, relevant validity boundaries, exception/omitted years, leap cases, durations, offsets and MOV/DUP/BASE behaviour. |
Text preservation |
Multiline base name, blank lines, embedded >>> away from line start, ordered/duplicate variations, multiline payload retention, <hol>, <DELETE>, & and literal . |
CDATA |
Empty text, Unicode, tabs, ]]> splitting, adjacent CDATA concatenation and writer indentation. Test with the supplied Delphi XML unit, not only a standard XML parser. |
Filter Year |
Old arbitrary dates, crossing date ranges, replacement and omitted exceptions, names and abbreviations; ensure XML and BLOB contents have the same cutoff. |
Invalid input |
NULL/empty/whitespace XML versus malformed nonblank XML; mismatched closing tags, duplicate fields, missing active branch, bad booleans/enums, out-of-range dates/IDs, overcapacity arrays and forbidden control characters. |
Modification metadata |
Storage conversion leaves semantic modification dates unchanged. Real XML edits trigger existing holiday/set notifications and correct modification tracking. |
Build/runtime |
Olivier has compiled and run the database round-trip harness locally. The optional HOLIDAY_XML_SELF_TEST suite is separate; execution of that suite is not claimed here. Native Help+Manual rendering and the end-to-end generation comparison remain to be verified. |
Open implementation details — tracked, not guessed
Item |
What is needed |
Unused nakshatra slot compatibility |
Nakshatra2 is unused in the supplied helper. Confirm other consumers and XE3 also ignore it before finalizing zero normalization; report nonzero legacy values until checked. |
Nakshatra calculation review |
The three result slots legitimately accommodate neighboring-year candidates. Recommended separately: initialize the fixed result array with Result := Default(ThiDateOccurences); at helper entry, then compile and verify unused slots and selected dates. Application/testing is not yet confirmed. Review CriticalTimeType=3 formula separately; do not change calculation behaviour as an incidental XML conversion. |
Calendar-specific units and legacy rows |
Calendar codecs now use explicit editor-compatible domains. Mansion values retain number 0..26 and fraction 0..59; longitude values retain exact thousandths. Stored expunged-day offsets and supported legacy flags are preserved. Calendar combinations that produce no occurrence are not rejected merely for producing no date. |
Reserved/unsupported settings |
Observance index 10 is resolved and preserved. Nonzero active hisNakshatra2 remains a compatibility-review error. Historical inactive rule types, Zoroastrian Kadmi/Fasli and unsupported target encodings remain unsupported; conflict target -1 has an explicit Unresolved representation. |
Formatting/control-character domain |
Confirm actual legacy Formatting and symbol-text control conventions before declaring full text coverage. The draft maps their fields and reports unexpected forbidden controls. |
Defaults for inactive legacy slots |
The reader starts with a fresh, initialized temporary holiday and sets inactive variant/reference fields to deterministic defaults. Round-trip equivalence concerns active semantic settings, not unused bytes in a variant record. |
XML helper verification |
The supplied VerySimple writer and strict MSXML 6 reader have been exercised by Olivier through the database round-trip harness. The reader validates well-formedness without relying on VerySimple closing-tag parsing. This does not claim execution of every optional self-test or production database integration. |
Full project write-path audit |
Audit all production readers and writers, including synchronization, shortcuts, generation, packaging and the actual database schema. The test harness and RN1/RN2 extraction do not establish that these write paths have all been converted. |
Current implementation: separate cHolidayXML.pas
cHolidayXML.pas contains SaveHolidayToXML, ReadHolidayFromXML, HolidayXMLIsBlank and ReadHolidayFromStorage. THoliday.SaveToXML and THoliday.ReadFromXML delegate to the codec. Serialization and validation are outside DataHolidays. The storage wrapper receives the actual XML TField and a complete legacy-loader callback; no memo-field name is assumed.
Implemented scope: complete common settings and all 22 active families: Fixed, NthWeekday, Easter, ArbitraryDates, DateRanges, Jewish, Muslim, Chinese, Korean, Vietnamese, Hindu, Tibetan, Bhutanese, Mongolian, Burmese, Egyptian, Zoroastrian, Persian, Sikh, SouthEastAsian, Sun and Lunisolar. Only active subtype/reference branches are written. Supporting serialization does not enable any previously unimplemented date calculator.
SaveHolidayToXML returns a Unicode string and does not post records. ReadHolidayFromXML validates a fresh temporary holiday before committing settings. The reader keeps ID, CountryID, DateModified and sort metadata from destination context, ignores whitelisted informational values, initializes inactive settings, and clears calculated DatesArray only after success. ReadHolidayFromStorage also uses a temporary object for the legacy callback. The existing read routines must finish historical RuleOptions conversion before XML export.
Errors still identify unsupported or unrepresentable settings: out-of-range ProvinceID or priority, image modes 4..6, reserved heading flags, unsupported conflict encodings, active Move with unspecified units, nonzero active hisNakshatra2, invalid text controls and malformed/overcapacity exception streams. Supported legacy sentinels, independent NoneSpecified and reversed validity bounds are preserved explicitly; they are not migration errors.
Reader syntax checking uses Windows MSXML 6 with DTDs prohibited and external resolution disabled; it copies the validated DOM into TXmlNode objects without invoking VerySimple parsing. The existing VerySimple writer remains unchanged. COM objects are local to the calling thread, with balanced initialization. Free text accepts ordinary escaped XML text or CDATA; CDATA plus non-whitespace ordinary text is rejected to avoid silently dropping content.
The unit includes an opt-in HOLIDAY_XML_SELF_TEST block. Define the symbol and call RunHolidayXMLSelfTests in the XV4 build. Tests cover canonical round trips, multiline names/variations, CDATA terminators, literal entity-like text and Unicode, the complete weekday-selector domain, schema errors, unchanged destination after failed reads, and NULL/blank/nonblank memo selection. These Delphi tests have not been executed here. Local checks validate source structure and XML fixtures only; they are not a substitute for Delphi compilation.
Next integration work: connect XML-first loading to complete legacy fallback; centralize paired XML/legacy saves and housekeeping; add the XML memo throughout schema/datasets/SQL; connect generation conversion and legacy resynchronization before XE3 packaging. Filter Year remains an explicit caller operation. Validate the full 2023–2055 generation against a legacy baseline before release.
// The field name is selected by the caller; no LoadVersion is consulted. |
MSXML loadXML and well-formedness
COM initialization and balancing
Decision record and next implementation milestones
Status |
Decision / milestone |
Agreed |
XML authority in XV4 when nonblank; NULL/blank uses legacy; paired saves; XE3 remains a legacy reader. |
Agreed |
Generation already visits every rule, including date-less rules. |
Agreed |
Full holiday representation, stable symbolic enums, explicit common defaults, dotted disambiguated element names, active variant only. |
Agreed |
Separate ordered variation nodes; reference name may be multiline; >>> at line start is the variation boundary. |
Agreed |
Filter Year covers name/abbreviation variations, arbitrary dates, date ranges and exceptions; apply the same result to both saved representations. |
Implemented and documented |
Complete field inventory and codec for all 22 active families; explicit legacy compatibility decisions and source-based mapping. Remaining integration and validation work is tracked separately. |
Resolved during review |
Weekday-selection codec, date semantics and UI mappings resolved from pasted frmWeekdaysBeforeAfter.pas and DFM. Occurrence range 1..99; default 1. The DFM dependency is closed. |
Resolved during review |
TPictureBoxAlignment ordinals 0..6, South-East Asian eras 1..6 and special rules 1..4. The supplied nakshatra helper uses only Nakshatra1; its flag prefers a later occurrence of that same nakshatra. |
Resolved during review |
Holiday image alignment supports only ordinals 0..3. Shared enum values 4..6 are excluded from holiday XML; use slogans for detailed positioning. |
Recommended correction |
Initialize the full nakshatra result array at function entry. Keep the three result slots and the second-occurrence replacement; Delphi application and verification remain pending. |
Clarified during review |
The three candidate-date slots support dates for year Y arising from Y-1, Y or Y+1 calculations. Preserve this return structure; the separate helper review concerns initialization of unused slots only. |
Implemented refinement |
Hindu solar XML uses nakshatra.number and nakshatra.preferSecondOccurrence. Nonzero active hisNakshatra2 is rejected pending compatibility review; no second nakshatra interval is inferred. |
Agreed naming |
camelCase within concepts; dots only for ownership/hierarchy. This revision updates the mapping and examples accordingly. |
Completed family testing |
Olivier reports successful database XML round trips for all 22 active rule types. The final Sun batch contained 162 rules and the final Lunisolar batch 38. |
Implemented codec |
Separate cHolidayXML.pas supports all active families, complete temporary-object reads and NULL/blank selection without LoadVersion. Tested locally in Delphi through the user database harness. |
Pending implementation |
Complete shared paired database persistence and explicit filtering, schema migration, and all UI/service integrations. All active family codecs are now present. |
Pending validation |
Final combined regression; XML-loaded worldwide generation for 2023–2055 compared with approximately 130,000 baseline dates and names; XML-to-legacy/XE3 packaging verification; native Help+Manual review. |
Before release |
Replace provisional claims with implemented behaviour, record supported schema versions and error messages, and move internal-only implementation notes to reference/maintenance topics if desired. |
Local source snapshot: cHoliday(1).pas, DataHolidays(1).pas, DataHolidays(1).dfm and Xml.VerySimple.pas supplied in this discussion, plus frmWeekdaysBeforeAfter.pas and its DFM pasted directly into the chat. Later pasted excerpts confirm TPictureBoxAlignment, the six South-East Asian era constants and names, four SpecialHolidays names, and the implementation of HinduSolarNakshatraInGregorianYear. The topic format and project style names are based on the existing HMXP_QppDocumentation_v64.zip, especially xmlref_conventions.xml. The archive is used as a format reference only; no existing topics or table of contents are replaced. This revision also uses cHolidayXML.pas version 24, the supplied cHoliday(2).pas, and Olivier’s subsequent test reports and clarifications of RN1/RN2 and annual expectation.
Synchronization and Filter Year
XML 1.0 syntax and allowed characters
Worked XML fragments — illustrative settings
These excerpts illustrate selected settings and omit other required sections; they are not complete importable holidays. All active rule families are now supported by the codec. No finalized XSD is supplied. Use a complete SaveToXML result when testing ReadFromXML.
02-Regional-Observance.xml — illustrative settings; common sections not shown remain required in a full holiday.
<observance> |
03-Common-Settings.xml — illustrative settings; common sections not shown remain required in a full holiday.
<rule.validity> |
04-Arbitrary-Dates.xml — illustrative settings; common sections not shown remain required in a full holiday.
<rule.type>ArbitraryDates</rule.type> |
05-Crossing-Date-Range.xml — illustrative settings; common sections not shown remain required in a full holiday.
<rule.type>DateRanges</rule.type> |
07-Hindu-Lunar.xml — illustrative settings; common sections not shown remain required in a full holiday.
<rule.type>Hindu</rule.type> |
08-Lunisolar-Reference.xml — illustrative settings; common sections not shown remain required in a full holiday.
<rule.type>Lunisolar</rule.type> |
For Filter Year 2026, example 04 loses only its 2025 date. Example 05 retains the entire range: its inclusive end is 4 January 2026, even though it starts in 2025. Examples 07 and 08 are schematic calendar settings, not claims about a particular real-world holiday.
Verification performed for this draft
A Python model checked all 2,773 canonical weekday-selector states in the confirmed editor domain (None plus four relations × seven weekdays × 99 occurrences), and compared the documented semantics against a direct translation of the pasted WeekdayAfterToDate for 16,632 date cases. Thirty-five encodings outside the proposed XML domain were rejected, including all 28 combinations for occurrence 100. This verifies the proposed formulas against the supplied logic; it is not a Delphi compile or execution test. XML examples, table dimensions, field coverage and representative multiline text round trips were checked separately.
Mechanical field-coverage check
The original inventory accounted for all 24 public THoliday data members, 178 THolidayRule members and 14 TRuleOptions members: 216 dispositions, including historical/padding fields. All 22 active family codecs are now implemented. Inventory coverage and successful round trips complement, but do not replace, the pending end-to-end calculation and database compatibility tests.