Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions main/SS/UserModel/ExcelStyleDateFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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/]*"))
Expand Down
86 changes: 67 additions & 19 deletions main/SS/Util/SheetUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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();
}
}

Expand Down
3 changes: 3 additions & 0 deletions testcases/main/HSSF/UserModel/TestCellStyle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");

Expand Down
2 changes: 1 addition & 1 deletion testcases/main/SS/UserModel/TestDataFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
33 changes: 33 additions & 0 deletions testcases/main/SS/UserModel/TestExcelStyleDateFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,38 @@ public void TestWithPattern()
new SimpleDateFormat("yyyy-MM-dd", ROOT).Parse("2016-03-26"), ROOT);
ClassicAssert.AreEqual("2016|M|", dateStr);
}

/// <summary>
/// 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.
/// </summary>
[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);
}
}
}