fields = date.getFields();
if (fields != null) {
for (Field field : fields) {
diff --git a/gedcomx-model/src/main/java/org/gedcomx/types/CalendarType.java b/gedcomx-model/src/main/java/org/gedcomx/types/CalendarType.java
new file mode 100644
index 000000000..52e136089
--- /dev/null
+++ b/gedcomx-model/src/main/java/org/gedcomx/types/CalendarType.java
@@ -0,0 +1,166 @@
+/**
+ * Copyright Intellectual Reserve, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.gedcomx.types;
+
+import com.webcohesion.enunciate.metadata.qname.XmlQNameEnum;
+
+/**
+ * Enumeration of calendar systems used for AlternateDate representations.
+ *
+ * Notes:
+ *
+ * - Descriptions here are concise overviews; actual conversion logic should be implemented
+ * in a dedicated date standardization service.
+ * - Examples include original scripts with romanization and translation for clarity.
+ *
+ */
+@XmlQNameEnum(
+ base = XmlQNameEnum.BaseType.URI
+)
+public enum CalendarType {
+
+ /**
+ * GREGORIAN — Proleptic Gregorian calendar.
+ *
+ * The standard civil calendar used in most of the world today, and the default calendar
+ * for GEDCOM X dates. This calendar is used for all “formal” GedcomX dates, and is
+ * assumed whenever a date does not explicitly specify a calendar type.
+ *
+ *
+ * The Gregorian calendar was introduced in 1582 to replace the Julian calendar.
+ * Different countries adopted it at different times, but in computational
+ * contexts it is treated as proleptic, meaning that its rules are
+ * extended backward to dates earlier than its historical adoption. Leap years
+ * occur in years divisible by 4, except century years not divisible by 400
+ * (e.g., 1900 is a common year; 2000 is a leap year).
+ *
+ * Example: October 15, 2025 (Gregorian)
+ */
+ Gregorian,
+
+ /**
+ * JULIAN — Julian calendar.
+ *
+ * A solar calendar introduced by Julius Caesar in 45 BCE, with a simple leap-year
+ * rule: every fourth year has 366 days. It was used throughout Europe until it was
+ * gradually replaced by the Gregorian calendar at different times in different
+ * regions.
+ *
+ *
+ * In genealogical records, dates in the early modern period may appear in “Old Style”
+ * (Julian) form, sometimes written as dual dates (e.g., 10 February 1740/41) because
+ * the legal new year began on March 25. Any such interpretation or conversion is
+ * handled by a date standardization service.
+ *
+ * Example: March 1, 1600 (Julian)
+ */
+ Julian,
+
+ /**
+ * FRENCH_REPUBLIC — French Republican calendar.
+ *
+ * A solar calendar introduced in 1792 with 12 months of 30 days named after seasonal features
+ * (e.g., Vendémiaire, Brumaire, Floréal), followed by 5 or 6 complementary days (sans‑culottides).
+ * Weeks are décades of 10 days.
+ *
+ * Example (French): 10 Floréal an II
+ * (dix Floréal an deux; 10 Floréal, Year 2)
+ */
+ FrenchRepublic,
+
+ /**
+ * CHINESE IMPERIAL — Traditional Chinese imperial (regnal) dating based on the lunisolar calendar.
+ *
+ * Dates are expressed with reign era names 年号 (niánhào; reign title), counting years within
+ * an emperor’s reign. Months and days follow the traditional lunisolar calendar, which can include
+ * leap months 闰月 (rùnyuè; leap month).
+ *
+ * Example (Chinese): 康熙四十五年 八月 初三
+ * (Kāngxī sìshíwǔ nián bāyuè chū sān; Kangxi year 45, 8th month, day 3)
+ */
+ ChineseImperial,
+
+ /**
+ * JAPANESE IMPERIAL — Japanese era-based regnal dating alongside Gregorian.
+ *
+ * Uses era names 元号 (gengō; era name) to count years within an emperor’s reign; the current era is
+ * 令和 (Reiwa; Beautiful Harmony) since 2019. Dates are written as
+ * “[era] [year]年 [month]月 [day]日”.
+ *
+ * Example (Japanese): 令和7年 3月 1日
+ * (Reiwa 7‑nen 3‑gatsu 1‑nichi; March 1, Reiwa 7)
+ */
+ JapaneseImperial,
+
+ /**
+ * KOREAN IMPERIAL — Korean imperial (regnal) calendar.
+ *
+ * Uses era names from the Daehan Empire for regnal dating, expressed as
+ * “[era name] [year] [month] [day]”. The empire name: 대한제국 (Daehan Jeguk; Great Han Empire).
+ * Format mirrors East Asian regnal styles.
+ *
+ * Example (Hangul): 광무 5년 3월 1일 (Gwangmu 5‑nyeon 3‑wol 1‑il; Gwangmu year 5, March 1)
+ */
+ KoreanImperial,
+
+ /**
+ * THAI SOLAR — Thai Buddhist solar calendar.
+ *
+ * Solar calendar aligned with Gregorian months and days; the year is counted in
+ * พุทธศักราช (Phutthasakkarat; Buddhist Era), where BE = CE + 543.
+ *
+ * Example (Thai): วันที่ 1 มกราคม พ.ศ. 2568
+ * (Wan thī 1 Mokkarakhom Phō.Sō. 2568; 1 January, BE 2568)
+ */
+ ThaiSolar,
+
+ /**
+ * Hebrew — Jewish lunisolar calendar.
+ *
+ * The Hebrew calendar הלוח העברי (ha-luach ha-ivri; the Hebrew calendar) is lunisolar:
+ * 12 months with a leap month added 7 times in a 19-year cycle (Metonic) to align
+ * lunar months with the solar year. Months begin at the new moon; religious observances
+ * follow the calendar’s rules for postponements and year lengths.
+ *
+ * Example (Hebrew): י״ד תשרי תשפ״ה
+ * (14 Tishri 5785; 14th of Tishri, year 5785)
+ */
+ Hebrew,
+
+ /**
+ * ISLAMIC — Islamic (Hijri) lunar calendar.
+ *
+ * A purely lunar calendar: 12 months of 29 or 30 days, totaling 354 or 355 days per year.
+ * Month starts traditionally depending on local crescent sighting; civil variants may use
+ * astronomical calculation. Common name: اَلْتَقْوِيمُ ٱلْهِجْرِي (al‑taqwīm al‑hijrī; the Hijri calendar).
+ *
+ * Example (Arabic): ١ رمضان ١٤٤٧ هـ (1 Ramaḍān 1447 AH; 1st of Ramadan, year 1447 of the Hijra)
+ */
+ Islamic,
+
+ /**
+ * RUMI — Rumi solar calendar (Turkish Ottoman Empire).
+ *
+ * A solar calendar used in late Ottoman administration, originally based on the Julian calendar.
+ * It employed fiscal year numbering and, in later reforms, incorporated Gregorian‑style adjustments
+ * while keeping Rumi year numbers. The name رومي (Rūmī; Roman/Byzantine) reflects its provenance.
+ *
+ * Example: Rûmî 1325, Kanun‑i Evvel 1
+ * (Rûmî 1325; December 1 in the Rumi year 1325)
+ */
+ Rumi;
+}
+
diff --git a/gedcomx-model/src/test/java/org/gedcomx/conclusion/AlternateCalendarDateTest.java b/gedcomx-model/src/test/java/org/gedcomx/conclusion/AlternateCalendarDateTest.java
new file mode 100644
index 000000000..0fff9a3d9
--- /dev/null
+++ b/gedcomx-model/src/test/java/org/gedcomx/conclusion/AlternateCalendarDateTest.java
@@ -0,0 +1,124 @@
+package org.gedcomx.conclusion;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.gedcomx.Gedcomx;
+import org.gedcomx.records.Field;
+import org.gedcomx.records.FieldValue;
+import org.gedcomx.records.HasFields;
+import org.gedcomx.rt.json.GedcomJacksonModule;
+import org.gedcomx.types.CalendarType;
+import org.gedcomx.types.FieldType;
+import org.gedcomx.types.FieldValueType;
+import org.gedcomx.util.MarshalUtil;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class AlternateCalendarDateTest {
+ @Test
+ public void testAlternateCalendarDate() {
+ Date date = new Date();
+ date.setOriginal("28 March 2021");
+ date.setFormal("+2021-03-28");
+ assertEquals("28 March 2021", date.getOriginal());
+ assertNull(date.getAlternateCalendars());
+
+ AlternateCalendarDate altDate = new AlternateCalendarDate(CalendarType.Hebrew);
+ altDate.setOriginal("15 Nisan 5781");
+ date.addAlternateCalendar(altDate);
+ assertEquals(1, date.getAlternateCalendars().size());
+ assertEquals("15 Nisan 5781", altDate.getOriginal());
+ assertEquals(CalendarType.Hebrew, altDate.getCalendar());
+ }
+
+ @Test
+ public void testBuilders() {
+ Date date = new Date();
+ date.setOriginal("10 February 1741");
+ date.setFormal("+1741-02-10");
+ assertNull(date.getAlternateCalendars());
+
+ AlternateCalendarDate altDate = new AlternateCalendarDate(CalendarType.Hebrew)
+ .calendar(CalendarType.Julian);
+ altDate.original("10 February 1740");
+ date.alternateCalendar(altDate);
+ assertEquals(1, date.getAlternateCalendars().size());
+ assertEquals("10 February 1741", date.getOriginal());
+ assertEquals("10 February 1740", altDate.getOriginal());
+ assertEquals(CalendarType.Julian, altDate.getCalendar());
+ }
+
+ @Test
+ public void testMarshallAlternateDate() throws JsonProcessingException {
+ Date date = new Date();
+ date.setOriginal("28 March 2021");
+ date.setFormal("+2021-03-28");
+ date.field(new Field().type(FieldType.Date)
+ .value(new FieldValue("28 March 2021").type(FieldValueType.Interpreted)));
+
+ AlternateCalendarDate altDate = new AlternateCalendarDate(CalendarType.Hebrew);
+ altDate.setOriginal("15 Nisan 5781");
+ altDate.field(new Field().type(FieldType.Date)
+ // Hebrew date in the original Hebrew script
+ .value(new FieldValue("ט״ו בְּנִיסָן ה׳תשפ״א").type(FieldValueType.Original))
+ // Same Hebrew date romanized
+ .value(new FieldValue("15 Nisan 5781").type(FieldValueType.Interpreted)));
+ date.addAlternateCalendar(altDate);
+
+ Gedcomx doc = new Gedcomx()
+ .person(new Person()
+ .fact(new Fact()
+ .date(date)));
+ //Test XML marshalling/unmarshalling
+ String xml = MarshalUtil.toXml(doc);
+ Gedcomx unmarshalledDoc = fromXml(xml);
+ checkAlternateDate(unmarshalledDoc, date, altDate);
+
+ //Test JSON marshalling/unmarshalling
+ ObjectMapper mapper = GedcomJacksonModule.createObjectMapper(Gedcomx.class);
+ String json = mapper.writeValueAsString(doc);
+ Gedcomx unmarshalledJsonDoc = mapper.readValue(json, Gedcomx.class);
+ checkAlternateDate(unmarshalledJsonDoc, date, altDate);
+
+ System.out.println("XML Output:\n" + xml);
+ System.out.println("JSON Output:\n" + json);
+ }
+
+ private static void checkAlternateDate(Gedcomx unmarshalledDoc, Date date, AlternateCalendarDate altDate) {
+ Date unmarshalledDate = unmarshalledDoc.getPersons().get(0).getFacts().get(0).getDate();
+ assertEquals(date.getOriginal(), unmarshalledDate.getOriginal());
+ assertEquals(date.getFormal(), unmarshalledDate.getFormal());
+ assertEquals(1, unmarshalledDate.getAlternateCalendars().size());
+ checkfields(date, unmarshalledDate);
+ AlternateCalendarDate unmarshalledAltDate = unmarshalledDate.getAlternateCalendars().get(0);
+ assertEquals(altDate.getOriginal(), unmarshalledAltDate.getOriginal());
+ assertEquals(altDate.getCalendar(), unmarshalledAltDate.getCalendar());
+ checkfields(altDate, unmarshalledAltDate);
+ }
+
+ private static void checkfields(HasFields expectedFields, HasFields actualFields) {
+ assertEquals(expectedFields.getFields().size(), actualFields.getFields().size());
+ for (int i = 0; i < expectedFields.getFields().size(); i++) {
+ Field expectedField = expectedFields.getFields().get(i);
+ Field actualField = actualFields.getFields().get(i);
+ assertEquals(expectedField.getType(), actualField.getType());
+ assertEquals(expectedField.getValues().size(), actualField.getValues().size());
+ for (int j = 0; j < expectedField.getValues().size(); j++) {
+ FieldValue expectedValue = expectedField.getValues().get(j);
+ FieldValue actualValue = actualField.getValues().get(j);
+ assertEquals(expectedValue.getType(), actualValue.getType());
+ assertEquals(expectedValue.getText(), actualValue.getText());
+ }
+ }
+ }
+
+ private Gedcomx fromXml(String xml) {
+ try {
+ return MarshalUtil.unmarshal(new java.io.ByteArrayInputStream(xml.getBytes()));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/gedcomx-model/src/test/java/org/gedcomx/util/MarshalUtil.java b/gedcomx-model/src/test/java/org/gedcomx/util/MarshalUtil.java
index a3ad7f6c6..52c6b359b 100755
--- a/gedcomx-model/src/test/java/org/gedcomx/util/MarshalUtil.java
+++ b/gedcomx-model/src/test/java/org/gedcomx/util/MarshalUtil.java
@@ -8,6 +8,9 @@
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import java.io.*;
+import javax.xml.transform.*;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
/**
* Convenience class for converting a GedcomX document or RecordSet to and from XML.
@@ -30,7 +33,11 @@ public class MarshalUtil {
private static Marshaller getMarshaller(boolean prettyFormat) throws JAXBException {
Marshaller m = jxbc.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, prettyFormat);
- m.setProperty("com.sun.xml.bind.indentString", " ");
+ try {
+ m.setProperty("com.sun.xml.bind.indentString", " ");
+ } catch (Exception e) {
+ // Ignore if not supported by the JAXB implementation
+ }
return m;
}
@@ -44,9 +51,8 @@ public static Unmarshaller createUnmarshaller() throws JAXBException {
* @param recordSet - RecordSet to write
* @param prettyFormat - flag for whether to use newlines and indentation in the output
* @throws JAXBException
- * @throws java.io.IOException
*/
- public static void output(OutputStream outputStream, RecordSet recordSet, boolean prettyFormat) throws JAXBException, IOException {
+ public static void output(OutputStream outputStream, RecordSet recordSet, boolean prettyFormat) throws JAXBException {
getMarshaller(prettyFormat).marshal(recordSet, outputStream);
}
@@ -56,9 +62,8 @@ public static void output(OutputStream outputStream, RecordSet recordSet, boolea
* @param doc - GedcomX document to write
* @param prettyFormat - flag for whether to use newlines and indentation in the output
* @throws JAXBException
- * @throws IOException
*/
- public static void output(OutputStream outputStream, Gedcomx doc, boolean prettyFormat) throws JAXBException, IOException {
+ public static void output(OutputStream outputStream, Gedcomx doc, boolean prettyFormat) throws JAXBException {
getMarshaller(prettyFormat).marshal(doc, outputStream);
}
@@ -80,9 +85,9 @@ public static Gedcomx unmarshal(InputStream inputStream) throws JAXBException {
public static String toXml(Gedcomx doc) {
try {
StringWriter sw = new StringWriter();
- getMarshaller(true).marshal(doc, sw);
- return sw.toString();
- } catch (JAXBException e) {
+ getMarshaller(false).marshal(doc, sw); // Disable JAXB pretty-printing
+ return formatXml(sw.toString(), 2);
+ } catch (Exception e) {
e.printStackTrace();
}
return null;
@@ -96,11 +101,30 @@ public static String toXml(Gedcomx doc) {
public static String toXml(RecordSet recordSet) {
try {
StringWriter sw = new StringWriter();
- getMarshaller(true).marshal(recordSet, sw);
- return sw.toString();
- } catch (JAXBException e) {
+ getMarshaller(false).marshal(recordSet, sw); // Disable JAXB pretty-printing
+ return formatXml(sw.toString(), 2);
+ } catch (Exception e) {
e.printStackTrace();
}
return null;
}
+
+ /**
+ * Utility to format XML with a specified indent (in spaces).
+ */
+ private static String formatXml(String xml, int indent) throws Exception {
+ TransformerFactory factory = TransformerFactory.newInstance();
+ try {
+ factory.setAttribute("indent-number", indent);
+ } catch (IllegalArgumentException ignored) {
+ // Some factories do not support this attribute
+ }
+ Transformer transformer = factory.newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
+ StringWriter writer = new StringWriter();
+ transformer.transform(new StreamSource(new StringReader(xml)), new StreamResult(writer));
+ // Collapse multiple newlines into a single newline to avoid extra blank lines
+ return writer.toString().replaceAll("(\r?\n){2,}", "\n");
+ }
}