sw/inc/formatcontentcontrol.hxx                         |    4 -
 sw/qa/extras/ooxmlexport/data/tdf169101_datePicker.docx |binary
 sw/qa/extras/ooxmlexport/ooxmlexport25.cxx              |   13 +++++
 sw/source/core/crsr/datecontentcontrolbutton.cxx        |    6 +-
 sw/source/core/txtnode/attrcontentcontrol.cxx           |   41 ++++++++--------
 sw/source/filter/ww8/docxattributeoutput.cxx            |   15 ++++-
 sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx    |    3 -
 7 files changed, 54 insertions(+), 28 deletions(-)

New commits:
commit 9a16f47ccb3ee9ff509a56bba026568f29800120
Author:     Justin Luth <[email protected]>
AuthorDate: Tue Feb 10 20:38:37 2026 -0500
Commit:     Justin Luth <[email protected]>
CommitDate: Thu Feb 12 07:32:53 2026 +0100

    tdf#169101 docx export: ensure valid ISO8601 Sdt fullDate
    
    If the fullDate value is not a valid ISO8601 format string
    then MS Word complains that the file is invalid.
    
    To fix this, I needed a mechanism similar to
      std::pair<bool, double> DateFieldmark::GetCurrentDate()
    that allows GetCurrentDate to know whether it is a valid date, and
      DateFieldmark::GetDateInStandardDateFormat
    to get the date in a standard format.
    
    In addition, I fixed a few other overlooked pieces.
    
    make CppunitTest_sw_ooxmlexport25 \
        CPPUNIT_TEST_NAME=testTdf169101_datePicker
    
    This unit test forced me to create the fall-back mechanism:
    make CppunitTest_sw_ooxmlexport13 \
        CPPUNIT_TEST_NAME=testInvalidDateFormField
    
    Change-Id: If6ea8022f82d70a10c9af9cede285855122ddf60
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199118
    Reviewed-by: Justin Luth <[email protected]>
    Tested-by: Jenkins

diff --git a/sw/inc/formatcontentcontrol.hxx b/sw/inc/formatcontentcontrol.hxx
index 91efffd11e28..a53d6b3b7329 100644
--- a/sw/inc/formatcontentcontrol.hxx
+++ b/sw/inc/formatcontentcontrol.hxx
@@ -292,10 +292,10 @@ public:
     void SetCurrentDateValue(double fCurrentDate);
 
     /// Parses m_aCurrentDate and returns it.
-    double GetCurrentDateValue() const;
+    std::optional<double> GetCurrentDateValue() const;
 
     /// Formats m_oSelectedDate, taking m_aDateFormat and m_aDateLanguage into 
account.
-    OUString GetDateString() const;
+    OUString GetDateString(bool bAsISO8601 = false) const;
 
     void SetPlainText(bool bPlainText) { m_bPlainText = bPlainText; }
 
diff --git a/sw/qa/extras/ooxmlexport/data/tdf169101_datePicker.docx 
b/sw/qa/extras/ooxmlexport/data/tdf169101_datePicker.docx
new file mode 100644
index 000000000000..0be76293c531
Binary files /dev/null and 
b/sw/qa/extras/ooxmlexport/data/tdf169101_datePicker.docx differ
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx 
b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx
index 0a4d2820d01e..c160cb326996 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport25.cxx
@@ -212,6 +212,19 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf168988_grabbagDatePicker)
                        u"                                     2012./2013.");
 }
 
+CPPUNIT_TEST_FIXTURE(Test, testTdf169101_datePicker)
+{
+    createSwDoc("tdf169101_datePicker.docx");
+
+    save(TestFilter::DOCX);
+    xmlDocUniquePtr pXmlDoc = parseExport(u"word/document.xml"_ustr);
+    assertXPath(pXmlDoc, "//w:sdt", 1); // only one sdt
+    assertXPath(pXmlDoc, "//w:sdt/w:sdtPr/w:date", 1); // it is a date content 
control
+    // there is no valid date, so fullDate must not be provided (or MS Word 
says 'corrupt file')
+    assertXPathNoAttribute(pXmlDoc, "//w:sdt/w:sdtPr/w:date", "fullDate");
+    assertXPathContent(pXmlDoc, "//w:sdt/w:sdtContent/w:r/w:t", u"Week #");
+}
+
 CPPUNIT_TEST_FIXTURE(Test, testTdf170389_manyTabstops)
 {
     createSwDoc("tdf170389_manyTabstops.odt");
diff --git a/sw/source/core/crsr/datecontentcontrolbutton.cxx 
b/sw/source/core/crsr/datecontentcontrolbutton.cxx
index 06f5f92cd691..26bc21bc99a0 100644
--- a/sw/source/core/crsr/datecontentcontrolbutton.cxx
+++ b/sw/source/core/crsr/datecontentcontrolbutton.cxx
@@ -50,10 +50,10 @@ void SwDateContentControlButton::LaunchPopup()
     if (m_pContentControl)
     {
         const Date& rNullDate = m_pNumberFormatter->GetNullDate();
-        double fCurrentDate = m_pContentControl->GetCurrentDateValue();
-        if (fCurrentDate != 0)
+        std::optional<double> ofCurrentDate = 
m_pContentControl->GetCurrentDateValue();
+        if (ofCurrentDate.has_value())
         {
-            m_xCalendar->set_date(rNullDate + sal_Int32(fCurrentDate));
+            m_xCalendar->set_date(rNullDate + sal_Int32(*ofCurrentDate));
         }
     }
 
diff --git a/sw/source/core/txtnode/attrcontentcontrol.cxx 
b/sw/source/core/txtnode/attrcontentcontrol.cxx
index c99b25b8b4f8..4f9d6407f5f2 100644
--- a/sw/source/core/txtnode/attrcontentcontrol.cxx
+++ b/sw/source/core/txtnode/attrcontentcontrol.cxx
@@ -369,41 +369,41 @@ void SwContentControl::ClearListItems()
         GetTextAttr()->Invalidate();
 }
 
-OUString SwContentControl::GetDateString() const
+OUString SwContentControl::GetDateString(bool bAsISO8601) const
 {
     SwDoc& rDoc = m_pTextNode->GetDoc();
     SvNumberFormatter* pNumberFormatter = rDoc.GetNumberFormatter();
-    sal_uInt32 nFormat = pNumberFormatter->GetEntryKey(
-        m_aDateFormat, LanguageTag(m_aDateLanguage).getLanguageType());
+    OUString aFormat = bAsISO8601 ? "YYYY-MM-DDTHH:MM:SSZ" : m_aDateFormat;
+    sal_uInt32 nFormat
+        = pNumberFormatter->GetEntryKey(aFormat, 
LanguageTag(m_aDateLanguage).getLanguageType());
 
     if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND)
     {
         // If not found, then create it.
         sal_Int32 nCheckPos = 0;
         SvNumFormatType nType;
-        OUString aFormat = m_aDateFormat;
         pNumberFormatter->PutEntry(aFormat, nCheckPos, nType, nFormat,
                                    
LanguageTag(m_aDateLanguage).getLanguageType());
     }
+    if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND)
+        return OUString();
 
-    const Color* pColor = nullptr;
-    OUString aFormatted;
-    double fSelectedDate = 0;
+    std::optional<double> ofSelectedDate;
     if (m_oSelectedDate)
     {
-        fSelectedDate = *m_oSelectedDate;
+        ofSelectedDate = *m_oSelectedDate;
     }
     else
     {
-        fSelectedDate = GetCurrentDateValue();
+        ofSelectedDate = GetCurrentDateValue();
     }
 
-    if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND)
-    {
+    if (!ofSelectedDate.has_value())
         return OUString();
-    }
 
-    pNumberFormatter->GetOutputString(fSelectedDate, nFormat, aFormatted, 
&pColor, false);
+    const Color* pColor = nullptr;
+    OUString aFormatted;
+    pNumberFormatter->GetOutputString(*ofSelectedDate, nFormat, aFormatted, 
&pColor, false);
     return aFormatted;
 }
 
@@ -430,34 +430,37 @@ void SwContentControl::SetCurrentDateValue(double 
fCurrentDate)
     const Color* pColor = nullptr;
     pNumberFormatter->GetOutputString(fCurrentDate, nFormat, aFormatted, 
&pColor, false);
     m_aCurrentDate = aFormatted + "T00:00:00Z";
+    m_aDateFormat = CURRENT_DATE_FORMAT;
 }
 
-double SwContentControl::GetCurrentDateValue() const
+std::optional<double> SwContentControl::GetCurrentDateValue() const
 {
     if (m_aCurrentDate.isEmpty())
     {
-        return 0;
+        return std::nullopt;
     }
 
     SwDoc& rDoc = m_pTextNode->GetDoc();
     SvNumberFormatter* pNumberFormatter = rDoc.GetNumberFormatter();
-    sal_uInt32 nFormat = pNumberFormatter->GetEntryKey(CURRENT_DATE_FORMAT, 
LANGUAGE_ENGLISH_US);
+    OUString sFormat = m_aDateFormat.isEmpty() ? CURRENT_DATE_FORMAT : 
m_aDateFormat;
+    sal_uInt32 nFormat = pNumberFormatter->GetEntryKey(sFormat, 
LANGUAGE_ENGLISH_US);
     if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND)
     {
         sal_Int32 nCheckPos = 0;
         SvNumFormatType nType;
-        OUString sFormat = CURRENT_DATE_FORMAT;
         pNumberFormatter->PutEntry(sFormat, nCheckPos, nType, nFormat, 
LANGUAGE_ENGLISH_US);
     }
 
     if (nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND)
     {
-        return 0;
+        return std::nullopt;
     }
 
     double dCurrentDate = 0;
     OUString aCurrentDate = m_aCurrentDate.replaceAll("T00:00:00Z", "");
-    (void)pNumberFormatter->IsNumberFormat(aCurrentDate, nFormat, 
dCurrentDate);
+    if (!pNumberFormatter->IsNumberFormat(aCurrentDate, nFormat, dCurrentDate))
+        return std::nullopt;
+
     return dCurrentDate;
 }
 
diff --git a/sw/source/filter/ww8/docxattributeoutput.cxx 
b/sw/source/filter/ww8/docxattributeoutput.cxx
index 2a5fda85a065..705f6cca86e5 100644
--- a/sw/source/filter/ww8/docxattributeoutput.cxx
+++ b/sw/source/filter/ww8/docxattributeoutput.cxx
@@ -2787,14 +2787,23 @@ void DocxAttributeOutput::WriteContentControlStart()
 
     if (m_pContentControl->GetDate())
     {
-        OUString aCurrentDate = m_pContentControl->GetCurrentDate();
-        if (aCurrentDate.isEmpty())
+        // fullDate must be a valid date (YYYY-MM-DDTHH:MM:SSZ) or MS Word 
calls the file corrupt
+        OUString aFullDate = 
m_pContentControl->GetDateString(/*bAsISO8601=*/true);
+        if (aFullDate.isEmpty())
+        {
+            // Round-trip fullDate (if valid) in case it just doesn't match 
the display format
+            const OUString sDisplayVal = m_pContentControl->GetCurrentDate();
+            if (sDisplayVal.getLength() == 20 && sDisplayVal[10] == 'T' && 
sDisplayVal[19] == 'Z')
+                aFullDate = sDisplayVal;
+        }
+
+        if (aFullDate.isEmpty())
         {
             m_pSerializer->startElementNS(XML_w, XML_date);
         }
         else
         {
-            m_pSerializer->startElementNS(XML_w, XML_date, FSNS(XML_w, 
XML_fullDate), aCurrentDate);
+            m_pSerializer->startElementNS(XML_w, XML_date, FSNS(XML_w, 
XML_fullDate), aFullDate);
         }
         OUString aDateFormat = 
m_pContentControl->GetDateFormat().replaceAll("\"", "'");
         if (!aDateFormat.isEmpty())
diff --git a/sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx 
b/sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx
index ffc2703608fb..614d1145dd06 100644
--- a/sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx
+++ b/sw/source/writerfilter/dmapper/DomainMapper_Impl.cxx
@@ -1271,7 +1271,8 @@ void DomainMapper_Impl::PopSdt()
     {
         OUString aDateString;
         xContentControl->getPropertyValue(u"DateString"_ustr) >>= aDateString;
-        xCursor->setString(aDateString);
+        if (!aDateString.isEmpty())
+            xCursor->setString(aDateString);
     }
 
     m_pSdtHelper->clear();

Reply via email to