diff --git a/main/SS/UserModel/ExcelStyleDateFormatter.cs b/main/SS/UserModel/ExcelStyleDateFormatter.cs index ac2f35463f..14fed202cf 100644 --- a/main/SS/UserModel/ExcelStyleDateFormatter.cs +++ b/main/SS/UserModel/ExcelStyleDateFormatter.cs @@ -129,6 +129,23 @@ public override string Format(object obj, CultureInfo culture) public StringBuilder Format(DateTime date, StringBuilder paramStringBuilder, CultureInfo culture) { + // If the pattern doesn't include milliseconds and doesn't use elapsed time brackets, + // round to the nearest second (Excel behavior: rounds half-up when no ms specifier) + bool hasMilliseconds = Pattern.Contains('f') || + Pattern.Contains('F') || + Pattern.Contains(L_BRACKET_SYMBOL) || + Pattern.Contains(LL_BRACKET_SYMBOL); + bool hasElapsedTime = Pattern.Contains(H_BRACKET_SYMBOL) || + Pattern.Contains(HH_BRACKET_SYMBOL) || + Pattern.Contains(M_BRACKET_SYMBOL) || + Pattern.Contains(MM_BRACKET_SYMBOL) || + Pattern.Contains(S_BRACKET_SYMBOL) || + Pattern.Contains(SS_BRACKET_SYMBOL); + if (!hasMilliseconds && !hasElapsedTime && date.Millisecond >= 500) + { + date = date.AddMilliseconds(1000 - date.Millisecond); + } + // Do the normal format string s = string.Empty; if (Regex.IsMatch(Pattern, "[yYmMdDhHsS\\-/,. :\"\\\\]+0?[ampAMP/]*")) diff --git a/main/SS/Util/SheetUtil.cs b/main/SS/Util/SheetUtil.cs index d7dfe3e44e..177e7c0f72 100644 --- a/main/SS/Util/SheetUtil.cs +++ b/main/SS/Util/SheetUtil.cs @@ -424,19 +424,32 @@ private static Font GetWindowsFont(ICell cell) private static double GetRotatedContentHeight(ICell cell, string stringValue, Font windowsFont) { var angle = cell.CellStyle.Rotation * 2.0 * Math.PI / 360.0; - var measureResult = TextMeasurer.MeasureAdvance(stringValue, new TextOptions(windowsFont) { Dpi = dpi }); - - var x1 = Math.Abs(measureResult.Height * Math.Cos(angle)); - var x2 = Math.Abs(measureResult.Width * Math.Sin(angle)); - - return Math.Round(x1 + x2, 0, MidpointRounding.ToEven); + try + { + var measureResult = TextMeasurer.MeasureAdvance(stringValue, new TextOptions(windowsFont) { Dpi = dpi }); + var x1 = Math.Abs(measureResult.Height * Math.Cos(angle)); + var x2 = Math.Abs(measureResult.Width * Math.Sin(angle)); + return Math.Round(x1 + x2, 0, MidpointRounding.ToEven); + } + catch (InvalidFontFileException) + { + // Broken system font; estimate height from font size in points converted to pixels + return Math.Round(windowsFont.Size * dpi / 72.0, 0, MidpointRounding.ToEven); + } } private static double GetContentHeight(string stringValue, Font windowsFont) { - var measureResult = TextMeasurer.MeasureAdvance(stringValue, new TextOptions(windowsFont) { Dpi = dpi }); - - return Math.Round(measureResult.Height, 0, MidpointRounding.ToEven); + try + { + var measureResult = TextMeasurer.MeasureAdvance(stringValue, new TextOptions(windowsFont) { Dpi = dpi }); + return Math.Round(measureResult.Height, 0, MidpointRounding.ToEven); + } + catch (InvalidFontFileException) + { + // Broken system font; estimate height from font size in points converted to pixels + return Math.Round(windowsFont.Size * dpi / 72.0, 0, MidpointRounding.ToEven); + } } /** @@ -537,16 +550,24 @@ private static double GetCellWidth(int defaultCharWidth, int colspan, { //Rectangle bounds; double actualWidth; - FontRectangle sf = TextMeasurer.MeasureSize(str, new TextOptions(windowsFont) { Dpi = dpi }); - if (style.Rotation != 0) + try { - double angle = style.Rotation * 2.0 * Math.PI / 360.0; - double x1 = Math.Abs(sf.Height * Math.Sin(angle)); - double x2 = Math.Abs(sf.Width * Math.Cos(angle)); - actualWidth = Math.Round(x1 + x2, 0, MidpointRounding.ToEven); + FontRectangle sf = TextMeasurer.MeasureSize(str, new TextOptions(windowsFont) { Dpi = dpi }); + if (style.Rotation != 0) + { + double angle = style.Rotation * 2.0 * Math.PI / 360.0; + double x1 = Math.Abs(sf.Height * Math.Sin(angle)); + double x2 = Math.Abs(sf.Width * Math.Cos(angle)); + actualWidth = Math.Round(x1 + x2, 0, MidpointRounding.ToEven); + } + else + actualWidth = Math.Round(sf.Width, 0, MidpointRounding.ToEven); + } + catch (InvalidFontFileException) + { + // Broken system font; use a rough estimate based on string length + actualWidth = str.Length * defaultCharWidth; } - else - actualWidth = Math.Round(sf.Width, 0, MidpointRounding.ToEven); int padding = 5; double correction = 1.05; @@ -615,7 +636,16 @@ public static int GetDefaultCharWidth(IWorkbook wb) IFont defaultFont = wb.GetFontAt((short)0); Font font = IFont2Font(defaultFont); - return (int)Math.Ceiling(TextMeasurer.MeasureSize(new string(defaultChar, 1), new TextOptions(font) { Dpi = dpi }).Width); + try + { + return (int)Math.Ceiling(TextMeasurer.MeasureSize(new string(defaultChar, 1), new TextOptions(font) { Dpi = dpi }).Width); + } + catch (InvalidFontFileException) + { + // Some system fonts (e.g. NotoColorEmoji on Linux) have missing tables. + // Fall back to a reasonable default character width. + return 8; + } } /** @@ -799,7 +829,25 @@ private static Font IFont2FontImpl(FontCacheKey cacheKey) throw new FontException("No fonts found installed on the machine."); } - fontFamily = SystemFonts.Families.First(); + // Pick the first system font that can be used for text measurement. + // Some fonts (e.g. NotoColorEmoji on Linux) have missing tables and + // will throw InvalidFontFileException when measuring text. + bool foundWorkingFont = false; + foreach (var candidate in SystemFonts.Families) + { + try + { + var testFont = new Font(candidate, 10); + TextMeasurer.MeasureSize("a", new TextOptions(testFont) { Dpi = 96 }); + fontFamily = candidate; + foundWorkingFont = true; + break; + } + catch (InvalidFontFileException) { } + } + + if (!foundWorkingFont) + fontFamily = SystemFonts.Families.First(); } } diff --git a/testcases/main/HSSF/UserModel/TestCellStyle.cs b/testcases/main/HSSF/UserModel/TestCellStyle.cs index 436dec70a6..e39a381153 100644 --- a/testcases/main/HSSF/UserModel/TestCellStyle.cs +++ b/testcases/main/HSSF/UserModel/TestCellStyle.cs @@ -530,6 +530,9 @@ public async Task TestNPOI1469() const int dop = 2; var time = DateTime.UtcNow.AddYears(-1); + // Truncate to whole second so DateTime.ToString and DataFormatter agree: + // DataFormatter rounds to nearest second per Excel behavior, so ms != 0 could cause mismatch. + time = new DateTime(time.Ticks / TimeSpan.TicksPerSecond * TimeSpan.TicksPerSecond, time.Kind); Console.WriteLine($"Start time: {time:yyyy/MM/dd} {time:HH:mm:ss}"); diff --git a/testcases/main/SS/UserModel/TestDataFormatter.cs b/testcases/main/SS/UserModel/TestDataFormatter.cs index 0862ab6d70..cbcff582b6 100644 --- a/testcases/main/SS/UserModel/TestDataFormatter.cs +++ b/testcases/main/SS/UserModel/TestDataFormatter.cs @@ -874,7 +874,7 @@ public void TestBug60031() DataFormatter dfUS = new DataFormatter(CultureInfo.GetCultureInfo("en-US")); ClassicAssert.AreEqual("2016-23-08 08:51:01", dfUS.FormatRawCellContents(42605.368761574071, -1, "yyyy-dd-MM HH:mm:ss")); ClassicAssert.AreEqual("2016-23 08:51:01 08", dfUS.FormatRawCellContents(42605.368761574071, -1, "yyyy-dd HH:mm:ss MM")); - ClassicAssert.AreEqual("2017-12-01 January 09:54:33", dfUS.FormatRawCellContents(42747.412892397523, -1, "yyyy-dd-MM MMMM HH:mm:ss")); + ClassicAssert.AreEqual("2017-12-01 January 09:54:34", dfUS.FormatRawCellContents(42747.412892397523, -1, "yyyy-dd-MM MMMM HH:mm:ss")); ClassicAssert.AreEqual("08", dfUS.FormatRawCellContents(42605.368761574071, -1, "MM")); ClassicAssert.AreEqual("01", dfUS.FormatRawCellContents(42605.368761574071, -1, "ss")); diff --git a/testcases/main/SS/UserModel/TestExcelStyleDateFormatter.cs b/testcases/main/SS/UserModel/TestExcelStyleDateFormatter.cs index 9b9b47d32c..26567d8c04 100644 --- a/testcases/main/SS/UserModel/TestExcelStyleDateFormatter.cs +++ b/testcases/main/SS/UserModel/TestExcelStyleDateFormatter.cs @@ -135,5 +135,38 @@ public void TestWithPattern() new SimpleDateFormat("yyyy-MM-dd", ROOT).Parse("2016-03-26"), ROOT); ClassicAssert.AreEqual("2016|M|", dateStr); } + + /// + /// Tests that when no milliseconds are present in the format pattern, + /// seconds are rounded to nearest (Excel behavior). + /// Value 44736.1070318287 represents 24.06.2022 02:34:07.550 in Excel. + /// + [Test] + public void TestSecondsRoundingWithoutMilliseconds() + { + // 24.06.2022 02:34:07.550 - 550ms should round up the second when ms not in pattern + DateTime date = new DateTime(2022, 6, 24, 2, 34, 7, 550); + double excelDate = 44736.1070318287; + + ExcelStyleDateFormatter fmtNoMs = new ExcelStyleDateFormatter("dd.MM.yyyy HH:mm:ss"); + fmtNoMs.SetDateToBeFormatted(excelDate); + String resultNoMs = fmtNoMs.Format(date, new StringBuilder(), ROOT).ToString(); + ClassicAssert.AreEqual("24.06.2022 02:34:08", resultNoMs); + + ExcelStyleDateFormatter fmtWith1Ms = new ExcelStyleDateFormatter("dd.MM.yyyy HH:mm:ss.0"); + fmtWith1Ms.SetDateToBeFormatted(excelDate); + String resultWith1Ms = fmtWith1Ms.Format(date, new StringBuilder(), ROOT).ToString(); + ClassicAssert.AreEqual("24.06.2022 02:34:07.5", resultWith1Ms); + + ExcelStyleDateFormatter fmtWith2Ms = new ExcelStyleDateFormatter("dd.MM.yyyy HH:mm:ss.00"); + fmtWith2Ms.SetDateToBeFormatted(excelDate); + String resultWith2Ms = fmtWith2Ms.Format(date, new StringBuilder(), ROOT).ToString(); + ClassicAssert.AreEqual("24.06.2022 02:34:07.55", resultWith2Ms); + + ExcelStyleDateFormatter fmtWith3Ms = new ExcelStyleDateFormatter("dd.MM.yyyy HH:mm:ss.000"); + fmtWith3Ms.SetDateToBeFormatted(excelDate); + String resultWith3Ms = fmtWith3Ms.Format(date, new StringBuilder(), ROOT).ToString(); + ClassicAssert.AreEqual("24.06.2022 02:34:07.550", resultWith3Ms); + } } } \ No newline at end of file