修复 Excel 无法表示 1900 年之前日期的问题
石头 authored at 2025-10-03 17:22:44
11.71 KiB
X
using System.ComponentModel;
using System.IO.Compression;
using System.Text;
using NewLife.IO;
using Xunit;

namespace XUnitTest.IO;

public class ExcelWriterTests
{
    [Fact, DisplayName("单Sheet全类型往返")]
    public void SingleSheet_AllTypes_Roundtrip()
    {
        using var ms = new MemoryStream();
        var writer = new ExcelWriter(ms);

        writer.WriteHeader(null!, new[] { "Name", "Percent", "Date", "DateTime", "Time", "Int", "Long", "Long2", "Long3", "DecFrac", "DecInt", "DoubleFrac", "BoolT", "BoolF", "BigNum", "LeadingZero", "IdCard", "PercentTextFail", "GapTest" });

        var dateOnly = new DateTime(2024, 7, 1);
        var dateTime = new DateTime(2024, 7, 1, 12, 34, 56);
        var time = TimeSpan.FromHours(5) + TimeSpan.FromMinutes(6) + TimeSpan.FromSeconds(7); // 05:06:07
        // 各列说明:Name(string), Percent("12.5%" 成功解析), Date(DateOnly), DateTime(Date+Time), Time(TimeSpan), Int, Long, DecFrac(有小数), DecInt(无小数), DoubleFrac, BoolT, BoolF, BigNum(>12位), LeadingZero(前导0), IdCard(含X), PercentTextFail("abc%"失败分支), GapTest(中间前面留一个null)
        var row = new Object?[]
        {
            "Alice", "12.5%", dateOnly, dateTime, time, 123, 2147483648L, 214748364899L, 2147483648999999L, 123.45m, 456m, 0.125d, true, false,
            "1234567890123", "00123", "12345619900101888X", "abc%", null
        };
        writer.WriteRows(null, new[] { row });

        writer.Save();

        File.WriteAllBytes("ew.xlsx", ms.ToArray());

        // 用 ExcelReader 读取验证类型与数值
        ms.Position = 0;
        var reader = new ExcelReader(ms, Encoding.UTF8);
        var rows = reader.ReadRows().ToList();
        Assert.Equal(2, rows.Count); // header + 1 数据行
        var header = rows[0].Select(e => e + "").ToArray();
        Assert.Equal("Name", header[0]);

        var data = rows[1];
        // Percent => Double 0.125
        Assert.Equal("Alice", data[0]);
        Assert.True(data[1] is Double && Math.Abs((Double)data[1]! - 0.125d) < 1e-9);
        Assert.True(data[2] is DateTime && ((DateTime)data[2]!).Date == dateOnly.Date && ((DateTime)data[2]!).TimeOfDay == TimeSpan.Zero);
        Assert.True(data[3] is DateTime && (DateTime)data[3]! == dateTime);
        Assert.True(data[4] is TimeSpan && (TimeSpan)data[4]! == time);
        Assert.Equal("123", data[5] + "");
        Assert.Equal(2147483648L, (Int64)data[6]!); // long
        Assert.Equal(214748364899L, (Int64)data[7]!); // long
        Assert.Equal("2147483648999999", data[8]!); // long
        Assert.True(data[9] is Decimal or Double); // 小数
        Assert.True(data[10] is Int32 or Int64 or Decimal); // 整数小数样式不变
        Assert.True(data[11] is Decimal or Double);
        Assert.True(data[12] is Boolean && (Boolean)data[12]!);
        Assert.True(data[13] is Boolean && !(Boolean)data[13]!);
        Assert.Equal("1234567890123", data[14]); // 大数字保留文本
        Assert.Equal("00123", data[15]); // 前导0保留
        Assert.Equal("12345619900101888X", data[16]); // 身份证含X
        Assert.Equal("abc%", data[17]); // 百分比解析失败 -> 文本
        // GapTest (最后列前提供 null) => ExcelWriter 跳过,读取时为缺失列应自动补 null
        Assert.Null(data[18]);
    }

    [Fact, DisplayName("多Sheet导出与读取")]
    public void MultiSheet_Export_Read()
    {
        using var ms = new MemoryStream();
        var w = new ExcelWriter(ms);
        w.WriteHeader("Users", new[] { "Id", "Name" });
        w.WriteRows("Users", new[] { new Object?[] { 1, "Tom" }, new Object?[] { 2, "Jerry" } });

        w.WriteHeader("Stats", new[] { "Metric", "Value" });
        w.WriteRows("Stats", new[] { new Object?[] { "Count", 2 }, new Object?[] { "Rate", "50%" } });
        w.Save();

        ms.Position = 0;
        var r = new ExcelReader(ms, Encoding.UTF8);
        var users = r.ReadRows("Users").ToList();
        Assert.Equal(3, users.Count); // header + 2
        var stats = r.ReadRows("Stats").ToList();
        Assert.Equal(3, stats.Count);

        // 百分比在第二个sheet中解析为 Double 0.5
        Assert.True(stats[2][1] is Double d && Math.Abs(d - 0.5) < 1e-9);
    }

    [Fact, DisplayName("无字符串时不生成sharedStrings")]
    public void NoSharedStrings_FileStructure()
    {
        using var ms = new MemoryStream();
        var w = new ExcelWriter(ms);
        // 全数字/日期/时间,不含字符串
        w.WriteRows(null, new[] { new Object?[] { 1, 2.5m, DateTime.Today, TimeSpan.FromMinutes(30) } });
        w.Save();

        ms.Position = 0;
        using var za = new ZipArchive(ms, ZipArchiveMode.Read, true, Encoding.UTF8);
        Assert.Null(za.GetEntry("xl/sharedStrings.xml")); // 不存在
        Assert.NotNull(za.GetEntry("xl/styles.xml"));
        Assert.NotNull(za.GetEntry("xl/worksheets/sheet1.xml"));
    }

    [Fact, DisplayName("Dispose自动保存文件路径")]
    public void Dispose_AutoSave_File()
    {
        var path = Path.Combine(Path.GetTempPath(), "excelwriter_test_" + Guid.NewGuid().ToString("N") + ".xlsx");
        try
        {
            using (var w = new ExcelWriter(path))
            {
                w.WriteHeader(null!, new[] { "A" });
                w.WriteRows(null, new[] { new Object?[] { 123 } });
            } // Dispose 触发保存

            using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            var r = new ExcelReader(fs, Encoding.UTF8);
            var rows = r.ReadRows().ToList();
            Assert.Equal(2, rows.Count);
            Assert.Equal("123", rows[1][0] + "");
        }
        finally
        {
            if (File.Exists(path)) File.Delete(path);
        }
    }

    [Fact, DisplayName("空Writer保存生成空Sheet")]
    public void EmptyWriter_Save()
    {
        using var ms = new MemoryStream();
        var w = new ExcelWriter(ms);
        w.Save();
        ms.Position = 0;
        var r = new ExcelReader(ms, Encoding.UTF8);
        var list = r.ReadRows().ToList();
        Assert.Empty(list); // 无数据行
    }

    [Fact, DisplayName("Int64使用整数样式避免科学计数")]
    public void Int64_Uses_Integer_Style()
    {
        using var ms = new MemoryStream();
        var w = new ExcelWriter(ms);
        w.WriteHeader(null!, new[] { "LongVal" });
        var longVal = 1234567890123456789L; // 19位,超过15位后将改为共享字符串,避免精度与科学计数
        w.WriteRows(null, new[] { new Object?[] { longVal } });
        w.Save();

        ms.Position = 0;
        using var za = new ZipArchive(ms, ZipArchiveMode.Read, true, Encoding.UTF8);
        var sheet = za.GetEntry("xl/worksheets/sheet1.xml");
        Assert.NotNull(sheet);
        using var sr = new StreamReader(sheet!.Open(), Encoding.UTF8);
        var xml = sr.ReadToEnd();
        // 现在超过15位的 Int64 以共享字符串方式写入,A1=表头(LongVal)->索引0,A2=长数字->索引1
        Assert.Contains("<c r=\"A2\" t=\"s\"><v>1</v></c>", xml);

        // 同时校验 sharedStrings.xml 中包含该长数字文本
        var sharedEntry = za.GetEntry("xl/sharedStrings.xml");
        Assert.NotNull(sharedEntry);
        using (var ssr = new StreamReader(sharedEntry!.Open(), Encoding.UTF8))
        {
            var sharedXml = ssr.ReadToEnd();
            Assert.Contains("LongVal", sharedXml);
            Assert.Contains(longVal.ToString(), sharedXml);
        }

        // 读取验证:返回为字符串(避免精度丢失),调用方可自行再解析
        ms.Position = 0;
        var r2 = new ExcelReader(ms, Encoding.UTF8);
        var rows = r2.ReadRows().ToList();
        Assert.Equal(longVal.ToString(), rows[1][0]);
        Assert.True(rows[1][0] is String);
    }

    [Fact, DisplayName("三个不同Sheet表头与数据互不干扰")]
    public void MultiSheet_ThreeSheets_DifferentHeaders_And_Data()
    {
        using var ms = new MemoryStream();
        var w = new ExcelWriter(ms);

        // Sheet 1: Users
        w.WriteHeader("Users", new[] { "UserId", "UserName", "Active" });
        w.WriteRows("Users", new[]
        {
            new Object?[] { 1, "Tom", true },
            new Object?[] { 2, "Jerry", false }
        });

        // Sheet 2: Orders
        w.WriteHeader("Orders", new[] { "OrderId", "Amount", "Date" });
        var orderDate = new DateTime(2024, 1, 2);
        w.WriteRows("Orders", new[]
        {
            new Object?[] { 1001, 123.45m, orderDate },
            new Object?[] { 1002, 200m, orderDate.AddDays(1) },
            new Object?[] { 1003, 0.5m, orderDate.AddDays(2) }
        });

        // Sheet 3: Logs (包含时间与文本混合,不同列数)
        w.WriteHeader("Logs", new[] { "Seq", "Level", "Message", "Time" });
        var t0 = DateTime.Now.Date.AddHours(8).AddMinutes(15).AddSeconds(30);
        w.WriteRows("Logs", new[]
        {
            new Object?[] { 1, "INFO", "Start", t0 },
            new Object?[] { 2, "WARN", "Latency", t0.AddMinutes(5) },
            new Object?[] { 3, "ERROR", "Failed", t0.AddMinutes(10) },
            new Object?[] { 4, "INFO", "Done", t0.AddMinutes(15) }
        });

        w.Save();

        File.WriteAllBytes("ew2.xlsx", ms.ToArray());

        ms.Position = 0;
        var r = new ExcelReader(ms, Encoding.UTF8);
        // 验证 sheet 名称集合包含三个
        var sheets = r.Sheets?.ToList();
        Assert.NotNull(sheets);
        Assert.Contains("Users", sheets!);
        Assert.Contains("Orders", sheets!);
        Assert.Contains("Logs", sheets!);

        // Users
        var users = r.ReadRows("Users").ToList();
        Assert.Equal(3, users.Count); // header + 2
        Assert.Equal("UserId", users[0][0]);
        Assert.Equal(1, users[1][0]);
        Assert.True(users[2][2] is Boolean && !(Boolean)users[2][2]!); // Active 列第二行 false

        // Orders
        var orders = r.ReadRows("Orders").ToList();
        Assert.Equal(4, orders.Count); // header + 3
        Assert.Equal("Amount", orders[0][1]);
        Assert.True(orders[2][2] is DateTime dt2 && dt2.Date == orderDate.AddDays(1).Date);
        Assert.True(orders[3][1] is Decimal or Double); // 金额小数

        // Logs
        var logs = r.ReadRows("Logs").ToList();
        Assert.Equal(5, logs.Count); // header + 4
        Assert.Equal("Level", logs[0][1]);
        Assert.Equal("ERROR", logs[3][1]); // 第3条日志(数据行 Seq=3)
        Assert.True(logs[4][3] is DateTime); // 时间列

        // 互不串表:确认 Users 的列数 != Logs 的列数
        Assert.NotEqual(users[0].Length, logs[0].Length);
    }

    [Fact, DisplayName("小于1900-01-01的日期写入为空字符串")]
    public void Date_Before_1900_Written_As_EmptyString()
    {
        using var ms = new MemoryStream();
        var w = new ExcelWriter(ms);
        w.WriteHeader(null!, new[] { "D1", "D2", "D3" });
        var invalidDate = new DateTime(1899, 12, 31);
        var boundaryDate = new DateTime(1900, 1, 1);
        var boundaryDateTime = new DateTime(1900, 1, 1, 12, 0, 0);
        w.WriteRows(null, new[] { new Object?[] { invalidDate, boundaryDate, boundaryDateTime } });
        w.Save();

        ms.Position = 0;
        var r = new ExcelReader(ms, Encoding.UTF8);
        var rows = r.ReadRows().ToList();
        Assert.Equal(2, rows.Count); // header + 1 数据行
        var data = rows[1];
        // 第1列:应为空字符串(共享字符串的空值)
        Assert.True(data[0] is String s && s.Length == 0);
        // 第2列:应解析为日期 1900-01-01
        Assert.True(data[1] is DateTime d1 && d1.Date == boundaryDate.Date && d1.TimeOfDay == TimeSpan.Zero);
        // 第3列:应解析为 日期时间 1900-01-01 12:00:00
        Assert.True(data[2] is DateTime d2 && d2 == boundaryDateTime);
    }
}