563 lines
26 KiB
C#
563 lines
26 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.IO.Compression;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Threading.Tasks;
|
||
using FootwearTest.Models;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace FootwearTest.Services;
|
||
|
||
public sealed class ExcelReportService
|
||
{
|
||
private readonly ILogger<ExcelReportService> _logger;
|
||
|
||
public ExcelReportService(ILogger<ExcelReportService> logger)
|
||
{
|
||
_logger = logger;
|
||
}
|
||
|
||
public async Task<string> ExportAsync(TestRunRecord record)
|
||
{
|
||
try
|
||
{
|
||
var directory = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
|
||
"整鞋试验报告");
|
||
Directory.CreateDirectory(directory);
|
||
|
||
var fileName = $"{record.CreatedAt:yyyyMMdd_HHmmss}_{SanitizeFileName(record.Method)}_{record.Id}.xlsx";
|
||
var path = Path.Combine(directory, fileName);
|
||
_logger.LogInformation("Exporting Excel report. RecordId={RecordId}, Method={Method}, Path={Path}", record.Id, record.Method, path);
|
||
await using var stream = File.Create(path);
|
||
WriteWorkbook(stream, CreateSheets(record));
|
||
_logger.LogInformation("Excel report exported. RecordId={RecordId}, Path={Path}", record.Id, path);
|
||
return path;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Failed to export Excel report. RecordId={RecordId}, Method={Method}", record.Id, record.Method);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private static IReadOnlyList<WorksheetData> CreateSheets(TestRunRecord record)
|
||
{
|
||
var sheets = new List<WorksheetData>
|
||
{
|
||
CreateCoverSheet(record),
|
||
CreateStandardChecklistSheet(record),
|
||
};
|
||
|
||
using var document = JsonDocument.Parse(record.DataJson);
|
||
var root = document.RootElement;
|
||
if (record.Method.StartsWith("方法 A", StringComparison.Ordinal))
|
||
{
|
||
sheets.Add(CreateMethodAResultSheet(root));
|
||
sheets.Add(CreateMethodASampleSheet(root));
|
||
}
|
||
else if (record.Method.StartsWith("方法 B", StringComparison.Ordinal))
|
||
{
|
||
sheets.Add(CreateMethodBResultSheet(root, record.Method));
|
||
if (record.Method.Contains("吸湿透水汽", StringComparison.Ordinal))
|
||
{
|
||
sheets.Add(CreateMethodBMassSheet(root));
|
||
}
|
||
|
||
sheets.Add(CreateProcedureSheet(root));
|
||
}
|
||
|
||
sheets.Add(CreateRawJsonSheet(root));
|
||
return sheets;
|
||
}
|
||
|
||
private static WorksheetData CreateCoverSheet(TestRunRecord record)
|
||
{
|
||
return new WorksheetData(
|
||
"报告封面",
|
||
[
|
||
Row("GB/T 33393-2023 整鞋试验报告"),
|
||
Row("报告项目", "内容"),
|
||
Row("记录编号", record.Id),
|
||
Row("文件编号", "GB/T 33393-2023"),
|
||
Row("使用的试验方法", record.Method),
|
||
Row("试样描述", record.SampleDescription),
|
||
Row("试验条件", record.Conditions),
|
||
Row("试验结果", record.ResultSummary),
|
||
Row("试验日期", record.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)),
|
||
Row("与试验方法的偏差", record.IsValid ? "无已知偏差" : "结果被标记为需复核或重测"),
|
||
]);
|
||
}
|
||
|
||
private static WorksheetData CreateStandardChecklistSheet(TestRunRecord record)
|
||
{
|
||
var rows = new List<IReadOnlyList<object?>>
|
||
{
|
||
Row("GB/T 33393-2023 核对项"),
|
||
Row("类别", "标准要求", "本次记录"),
|
||
Row("报告", "报告包含文件编号、方法、试样描述、试验条件、试验结果、日期和偏差", "已写入报告封面"),
|
||
};
|
||
|
||
if (record.Method.StartsWith("方法 A", StringComparison.Ordinal))
|
||
{
|
||
rows.Add(Row("方法 A 试样", "试样数量 2 只;试验前按 GB/T 22049 标准环境调节 24 h", record.SampleDescription));
|
||
rows.Add(Row("方法 A 环境", "环境 (23±2) ℃、(50±5)% RH;空气流速 (1.00±0.15) m/s", record.Conditions));
|
||
rows.Add(Row("方法 A 假脚", "假脚/模拟皮肤温度 (35.0±0.3) ℃;水汽化热 0.672 W·h/g", record.Conditions));
|
||
rows.Add(Row("方法 A 采样", "每分钟记录一次,连续记录至少 30 次;结果取连续 30 次平均值", "见“方法A连续数据”"));
|
||
rows.Add(Row("方法 A 偏差", "任一测定值与两只鞋测试结果平均值的偏差不应超过 ±10%", record.IsValid ? "通过" : "需复核或重测"));
|
||
}
|
||
else if (record.Method.Contains("吸湿透水汽", StringComparison.Ordinal))
|
||
{
|
||
rows.Add(Row("方法 B 试样", "满帮鞋同号两只;试验前在 (23±2) ℃、(50±5)% RH 标准环境调节", record.SampleDescription));
|
||
rows.Add(Row("方法 B 供水", "泵流量 (5.0±0.3) cm3/h;测试周期 (180±1) min", record.Conditions));
|
||
rows.Add(Row("方法 B 称重", "m11-m72 均精确到 0.01 g;m180 应在 (15±0.9) g 范围内", record.IsValid ? "m180 有效" : "m180 超限"));
|
||
rows.Add(Row("方法 B 结果", "两次试验结果取平均;透水汽性能 T180*、吸湿透水汽性能 XT180*=T180*+m3*+m4*,最终保留两位小数", record.ResultSummary));
|
||
}
|
||
else
|
||
{
|
||
rows.Add(Row("方法 B 保暖", "假脚温度 (38±1) ℃;测试周期 (180±1) min,连续测试 2 个周期", record.Conditions));
|
||
rows.Add(Row("方法 B 热阻", "每个周期 Q=P/t×1000、R=S×(Tf-Tc)/Q;最终取两次热阻平均值并保留三位小数", record.ResultSummary));
|
||
}
|
||
|
||
return new WorksheetData("标准核对", rows);
|
||
}
|
||
|
||
private static WorksheetData CreateMethodAResultSheet(JsonElement root)
|
||
{
|
||
var result = root.GetProperty("Result");
|
||
return new WorksheetData(
|
||
"方法A结果",
|
||
[
|
||
Row("项目", "数值", "单位/说明"),
|
||
Row("阶段", GetString(result, "Stage"), ""),
|
||
Row("皮肤湿阻 Res", GetDouble(result, "SkinMoistureResistance"), "Pa·m2/W"),
|
||
Row("整鞋平均湿阻 Re", GetDouble(result, "AverageMoistureResistance"), "Pa·m2/W"),
|
||
Row("整鞋平均热阻 Rt", GetDouble(result, "AverageThermalResistance"), "m2·℃/W"),
|
||
Row("湿阻变异系数", GetDouble(result, "MoistureCoefficientOfVariation"), "%"),
|
||
Row("热阻变异系数", GetDouble(result, "ThermalCoefficientOfVariation"), "%"),
|
||
Row("±10% 偏差检查", GetBool(result, "PassedDeviationCheck") ? "通过" : "需复核或重测", ""),
|
||
Row("湿热量 He", "He=λ×Q,λ=0.672 W·h/g", "W"),
|
||
Row("湿阻 Re", "Re=A×(Psi×RHsi-Pa×RHa)/He-Res", "Pa·m2/W"),
|
||
Row("干热量 Hd", "Hd=Hs-He", "W"),
|
||
Row("热阻 Rt", "Rt=A×(Ts-Ta)/Hd", "m2·℃/W"),
|
||
]);
|
||
}
|
||
|
||
private static WorksheetData CreateMethodASampleSheet(JsonElement root)
|
||
{
|
||
var rows = new List<IReadOnlyList<object?>>
|
||
{
|
||
Row("序号", "时间", "假脚温度 ℃", "环境温度 ℃", "相对湿度 %", "出汗量 g/h", "加热功率 W", "湿热量 He W", "干热量 Hd W", "湿阻 Pa·m2/W", "热阻 m2·℃/W"),
|
||
};
|
||
|
||
if (root.TryGetProperty("Samples", out var samples) && samples.ValueKind == JsonValueKind.Array)
|
||
{
|
||
foreach (var sample in samples.EnumerateArray())
|
||
{
|
||
var waterLoss = GetDouble(sample, "WaterLossGramsPerHour");
|
||
var power = GetDouble(sample, "PowerWatts");
|
||
var moistureHeat = sample.TryGetProperty("MoistureHeatWatts", out _) ? GetDouble(sample, "MoistureHeatWatts") : (waterLoss > 0 ? Round(0.672 * waterLoss, 3) : 0);
|
||
var dryHeat = sample.TryGetProperty("DryHeatWatts", out _) ? GetDouble(sample, "DryHeatWatts") : (power > moistureHeat ? Round(power - moistureHeat, 3) : 0);
|
||
rows.Add(Row(
|
||
GetDouble(sample, "Index"),
|
||
GetString(sample, "Timestamp"),
|
||
GetDouble(sample, "FootTemperatureC"),
|
||
GetDouble(sample, "EnvironmentTemperatureC"),
|
||
GetDouble(sample, "RelativeHumidityPercent"),
|
||
waterLoss,
|
||
power,
|
||
moistureHeat,
|
||
dryHeat,
|
||
GetDouble(sample, "MoistureResistance"),
|
||
GetDouble(sample, "ThermalResistance")));
|
||
}
|
||
}
|
||
|
||
return new WorksheetData("方法A连续数据", rows);
|
||
}
|
||
|
||
private static WorksheetData CreateMethodBResultSheet(JsonElement root, string method)
|
||
{
|
||
var result = root.GetProperty("Result");
|
||
var rows = new List<IReadOnlyList<object?>>
|
||
{
|
||
Row("项目", "数值", "单位/说明"),
|
||
};
|
||
|
||
if (method.Contains("吸湿透水汽", StringComparison.Ordinal))
|
||
{
|
||
if (result.TryGetProperty("Trial1", out var trial1) && result.TryGetProperty("Trial2", out var trial2))
|
||
{
|
||
rows.Add(Row("T180* 平均值", GetDouble(result, "AverageT180Corrected"), "g,透水汽性能,最终保留两位小数"));
|
||
rows.Add(Row("XT180* 平均值", GetDouble(result, "AverageXT180Corrected"), "g,吸湿透水汽性能,最终保留两位小数"));
|
||
rows.Add(Row("有效性", GetBool(result, "IsValid") ? "有效" : "至少一次 m180 超出 15±0.9 g", ""));
|
||
AddMoistureTrialRows(rows, "第1次", trial1);
|
||
AddMoistureTrialRows(rows, "第2次", trial2);
|
||
}
|
||
else
|
||
{
|
||
AddMoistureTrialRows(rows, "单次", result);
|
||
rows.Add(Row("有效性", GetBool(result, "IsValid") ? "有效" : "m180 超出 15±0.9 g", ""));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (result.TryGetProperty("Cycle1", out var cycle1) && result.TryGetProperty("Cycle2", out var cycle2))
|
||
{
|
||
rows.Add(Row("R 平均值", GetDouble(result, "AverageThermalResistance"), "m2·℃/W,最终保留三位小数"));
|
||
AddWarmthCycleRows(rows, "第1周期", cycle1);
|
||
AddWarmthCycleRows(rows, "第2周期", cycle2);
|
||
}
|
||
else
|
||
{
|
||
AddWarmthCycleRows(rows, "单周期", result);
|
||
}
|
||
}
|
||
|
||
return new WorksheetData("方法B结果", rows);
|
||
}
|
||
|
||
private static void AddMoistureTrialRows(List<IReadOnlyList<object?>> rows, string prefix, JsonElement trial)
|
||
{
|
||
rows.Add(Row($"{prefix} m5", GetDouble(trial, "M5"), "g,C1 消耗水量"));
|
||
rows.Add(Row($"{prefix} m6", GetDouble(trial, "M6"), "g,C2 蒸发水量"));
|
||
rows.Add(Row($"{prefix} m7", GetDouble(trial, "M7"), "g,C3 蒸发水量"));
|
||
rows.Add(Row($"{prefix} m8", GetDouble(trial, "M8"), "g,C2/C3 平均蒸发量"));
|
||
rows.Add(Row($"{prefix} m180", GetDouble(trial, "M180"), "g,泵进鞋腔内水总质量,15±0.9 g"));
|
||
rows.Add(Row($"{prefix} m1", GetDouble(trial, "M1"), "g,模拟皮肤质量变化"));
|
||
rows.Add(Row($"{prefix} m2", GetDouble(trial, "M2"), "g,标准长筒袜质量变化"));
|
||
rows.Add(Row($"{prefix} m3", GetDouble(trial, "M3"), "g,样品鞋质量变化"));
|
||
rows.Add(Row($"{prefix} m4", GetDouble(trial, "M4"), "g,鞋垫质量变化"));
|
||
rows.Add(Row($"{prefix} T180", GetDouble(trial, "T180"), "g,散发到环境中的水汽质量"));
|
||
rows.Add(Row($"{prefix} m1*", GetDouble(trial, "M1Corrected"), "g,按 15 g 修正"));
|
||
rows.Add(Row($"{prefix} m2*", GetDouble(trial, "M2Corrected"), "g,按 15 g 修正"));
|
||
rows.Add(Row($"{prefix} m3*", GetDouble(trial, "M3Corrected"), "g,按 15 g 修正"));
|
||
rows.Add(Row($"{prefix} m4*", GetDouble(trial, "M4Corrected"), "g,按 15 g 修正"));
|
||
rows.Add(Row($"{prefix} T180*", GetDouble(trial, "T180Corrected"), "g,透水汽性能"));
|
||
rows.Add(Row($"{prefix} XT180*", GetDouble(trial, "XT180Corrected"), "g,吸湿透水汽性能"));
|
||
}
|
||
|
||
private static void AddWarmthCycleRows(List<IReadOnlyList<object?>> rows, string prefix, JsonElement cycle)
|
||
{
|
||
rows.Add(Row($"{prefix} P", GetDouble(cycle, "EnergyKilojoules"), "kJ,180 min 能量消耗"));
|
||
rows.Add(Row($"{prefix} t", GetDouble(cycle, "Seconds"), "s,测试周期"));
|
||
rows.Add(Row($"{prefix} Q", GetDouble(cycle, "HeatWatts"), "W,单位时间热量消耗"));
|
||
rows.Add(Row($"{prefix} Tf", GetDouble(cycle, "FootTemperatureC"), "℃,假脚表面温度平均值"));
|
||
rows.Add(Row($"{prefix} Tc", GetDouble(cycle, "ChamberTemperatureC"), "℃,测试箱环境温度平均值"));
|
||
rows.Add(Row($"{prefix} R", GetDouble(cycle, "ThermalResistance"), "m2·℃/W,保暖性能热阻值"));
|
||
}
|
||
|
||
private static WorksheetData CreateMethodBMassSheet(JsonElement root)
|
||
{
|
||
var rows = new List<IReadOnlyList<object?>>
|
||
{
|
||
Row("字段", "数值 g", "说明"),
|
||
};
|
||
|
||
if (root.TryGetProperty("Masses", out var masses))
|
||
{
|
||
if (masses.TryGetProperty("Trial1", out var trial1) && masses.TryGetProperty("Trial2", out var trial2))
|
||
{
|
||
AddMassRows(rows, "第1次", trial1);
|
||
AddMassRows(rows, "第2次", trial2);
|
||
}
|
||
else
|
||
{
|
||
AddMassRows(rows, "", masses);
|
||
}
|
||
}
|
||
|
||
return new WorksheetData("方法B称重", rows);
|
||
}
|
||
|
||
private static void AddMassRows(List<IReadOnlyList<object?>> rows, string prefix, JsonElement masses)
|
||
{
|
||
var label = string.IsNullOrWhiteSpace(prefix) ? "" : $"{prefix} ";
|
||
rows.Add(Row($"{label}m11", GetDouble(masses, "M11"), "模拟皮肤测试前质量"));
|
||
rows.Add(Row($"{label}m12", GetDouble(masses, "M12"), "模拟皮肤测试后质量"));
|
||
rows.Add(Row($"{label}m21", GetDouble(masses, "M21"), "标准长筒袜测试前质量"));
|
||
rows.Add(Row($"{label}m22", GetDouble(masses, "M22"), "标准长筒袜测试后质量"));
|
||
rows.Add(Row($"{label}m31", GetDouble(masses, "M31"), "样品鞋测试前质量"));
|
||
rows.Add(Row($"{label}m32", GetDouble(masses, "M32"), "样品鞋测试后质量"));
|
||
rows.Add(Row($"{label}m41", GetDouble(masses, "M41"), "鞋垫测试前质量"));
|
||
rows.Add(Row($"{label}m42", GetDouble(masses, "M42"), "鞋垫测试后质量"));
|
||
rows.Add(Row($"{label}m51", GetDouble(masses, "M51"), "C1 测试前质量"));
|
||
rows.Add(Row($"{label}m52", GetDouble(masses, "M52"), "C1 测试后质量"));
|
||
rows.Add(Row($"{label}m61", GetDouble(masses, "M61"), "C2 测试前质量"));
|
||
rows.Add(Row($"{label}m62", GetDouble(masses, "M62"), "C2 测试后质量"));
|
||
rows.Add(Row($"{label}m71", GetDouble(masses, "M71"), "C3 测试前质量"));
|
||
rows.Add(Row($"{label}m72", GetDouble(masses, "M72"), "C3 测试后质量"));
|
||
}
|
||
|
||
private static WorksheetData CreateProcedureSheet(JsonElement root)
|
||
{
|
||
var rows = new List<IReadOnlyList<object?>>
|
||
{
|
||
Row("序号", "流程记录"),
|
||
};
|
||
|
||
if (root.TryGetProperty("ProcedureLog", out var log) && log.ValueKind == JsonValueKind.Array)
|
||
{
|
||
var index = 1;
|
||
foreach (var item in log.EnumerateArray())
|
||
{
|
||
rows.Add(Row(index++, item.GetString() ?? ""));
|
||
}
|
||
}
|
||
|
||
return new WorksheetData("流程记录", rows);
|
||
}
|
||
|
||
private static WorksheetData CreateRawJsonSheet(JsonElement root)
|
||
{
|
||
var rows = new List<IReadOnlyList<object?>>
|
||
{
|
||
Row("路径", "值"),
|
||
};
|
||
FlattenJson(rows, "", root);
|
||
return new WorksheetData("完整原始数据", rows);
|
||
}
|
||
|
||
private static void FlattenJson(List<IReadOnlyList<object?>> rows, string path, JsonElement element)
|
||
{
|
||
switch (element.ValueKind)
|
||
{
|
||
case JsonValueKind.Object:
|
||
foreach (var property in element.EnumerateObject())
|
||
{
|
||
var nextPath = string.IsNullOrWhiteSpace(path) ? property.Name : $"{path}.{property.Name}";
|
||
FlattenJson(rows, nextPath, property.Value);
|
||
}
|
||
break;
|
||
case JsonValueKind.Array:
|
||
var index = 0;
|
||
foreach (var item in element.EnumerateArray())
|
||
{
|
||
FlattenJson(rows, $"{path}[{index++}]", item);
|
||
}
|
||
break;
|
||
case JsonValueKind.Number:
|
||
rows.Add(Row(path, element.TryGetDouble(out var number) ? number : element.GetRawText()));
|
||
break;
|
||
case JsonValueKind.True:
|
||
case JsonValueKind.False:
|
||
rows.Add(Row(path, element.GetBoolean() ? "true" : "false"));
|
||
break;
|
||
case JsonValueKind.String:
|
||
rows.Add(Row(path, element.GetString() ?? ""));
|
||
break;
|
||
default:
|
||
rows.Add(Row(path, ""));
|
||
break;
|
||
}
|
||
}
|
||
|
||
private static void WriteWorkbook(Stream stream, IReadOnlyList<WorksheetData> sheets)
|
||
{
|
||
using var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true);
|
||
AddText(archive, "[Content_Types].xml", CreateContentTypes(sheets.Count));
|
||
AddText(archive, "_rels/.rels", """
|
||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||
</Relationships>
|
||
""");
|
||
AddText(archive, "xl/workbook.xml", CreateWorkbookXml(sheets));
|
||
AddText(archive, "xl/_rels/workbook.xml.rels", CreateWorkbookRelationships(sheets.Count));
|
||
AddText(archive, "xl/styles.xml", """
|
||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||
<fonts count="1"><font><sz val="11"/><name val="Microsoft YaHei"/></font></fonts>
|
||
<fills count="1"><fill><patternFill patternType="none"/></fill></fills>
|
||
<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
|
||
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
|
||
<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
|
||
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
|
||
</styleSheet>
|
||
""");
|
||
|
||
for (var i = 0; i < sheets.Count; i++)
|
||
{
|
||
AddText(archive, $"xl/worksheets/sheet{i + 1}.xml", CreateWorksheetXml(sheets[i]));
|
||
}
|
||
}
|
||
|
||
private static string CreateContentTypes(int sheetCount)
|
||
{
|
||
var builder = new StringBuilder();
|
||
builder.Append("""
|
||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
||
""");
|
||
for (var i = 1; i <= sheetCount; i++)
|
||
{
|
||
builder.Append(CultureInfo.InvariantCulture, $" <Override PartName=\"/xl/worksheets/sheet{i}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>\n");
|
||
}
|
||
|
||
builder.Append("</Types>");
|
||
return builder.ToString();
|
||
}
|
||
|
||
private static string CreateWorkbookXml(IReadOnlyList<WorksheetData> sheets)
|
||
{
|
||
var builder = new StringBuilder();
|
||
builder.Append("""
|
||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||
<sheets>
|
||
""");
|
||
for (var i = 0; i < sheets.Count; i++)
|
||
{
|
||
builder.Append(CultureInfo.InvariantCulture, $" <sheet name=\"{EscapeXml(TrimSheetName(sheets[i].Name))}\" sheetId=\"{i + 1}\" r:id=\"rId{i + 1}\"/>\n");
|
||
}
|
||
|
||
builder.Append("""
|
||
</sheets>
|
||
</workbook>
|
||
""");
|
||
return builder.ToString();
|
||
}
|
||
|
||
private static string CreateWorkbookRelationships(int sheetCount)
|
||
{
|
||
var builder = new StringBuilder();
|
||
builder.Append("""
|
||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
""");
|
||
for (var i = 1; i <= sheetCount; i++)
|
||
{
|
||
builder.Append(CultureInfo.InvariantCulture, $" <Relationship Id=\"rId{i}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet{i}.xml\"/>\n");
|
||
}
|
||
|
||
builder.Append(CultureInfo.InvariantCulture, $" <Relationship Id=\"rId{sheetCount + 1}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>\n");
|
||
builder.Append("</Relationships>");
|
||
return builder.ToString();
|
||
}
|
||
|
||
private static string CreateWorksheetXml(WorksheetData sheet)
|
||
{
|
||
var builder = new StringBuilder();
|
||
builder.Append("""
|
||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||
<sheetViews><sheetView workbookViewId="0"/></sheetViews>
|
||
<sheetFormatPr defaultRowHeight="18"/>
|
||
<cols>
|
||
<col min="1" max="1" width="22" customWidth="1"/>
|
||
<col min="2" max="20" width="24" customWidth="1"/>
|
||
</cols>
|
||
<sheetData>
|
||
""");
|
||
|
||
for (var rowIndex = 0; rowIndex < sheet.Rows.Count; rowIndex++)
|
||
{
|
||
var rowNumber = rowIndex + 1;
|
||
builder.Append(CultureInfo.InvariantCulture, $" <row r=\"{rowNumber}\">\n");
|
||
var row = sheet.Rows[rowIndex];
|
||
for (var columnIndex = 0; columnIndex < row.Count; columnIndex++)
|
||
{
|
||
AppendCell(builder, row[columnIndex], rowNumber, columnIndex + 1);
|
||
}
|
||
|
||
builder.Append(" </row>\n");
|
||
}
|
||
|
||
builder.Append("""
|
||
</sheetData>
|
||
</worksheet>
|
||
""");
|
||
return builder.ToString();
|
||
}
|
||
|
||
private static void AppendCell(StringBuilder builder, object? value, int row, int column)
|
||
{
|
||
var reference = $"{ColumnName(column)}{row}";
|
||
switch (value)
|
||
{
|
||
case null:
|
||
builder.Append(CultureInfo.InvariantCulture, $" <c r=\"{reference}\"/>\n");
|
||
break;
|
||
case byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal:
|
||
builder.Append(CultureInfo.InvariantCulture, $" <c r=\"{reference}\"><v>{Convert.ToString(value, CultureInfo.InvariantCulture)}</v></c>\n");
|
||
break;
|
||
default:
|
||
builder.Append(CultureInfo.InvariantCulture, $" <c r=\"{reference}\" t=\"inlineStr\"><is><t>{EscapeXml(Convert.ToString(value, CultureInfo.InvariantCulture) ?? "")}</t></is></c>\n");
|
||
break;
|
||
}
|
||
}
|
||
|
||
private static void AddText(ZipArchive archive, string name, string content)
|
||
{
|
||
var entry = archive.CreateEntry(name, CompressionLevel.Optimal);
|
||
using var writer = new StreamWriter(entry.Open(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||
writer.Write(content);
|
||
}
|
||
|
||
private static IReadOnlyList<object?> Row(params object?[] values) => values;
|
||
|
||
private static double GetDouble(JsonElement element, string property)
|
||
{
|
||
return element.TryGetProperty(property, out var value) && value.TryGetDouble(out var number) ? number : 0.0;
|
||
}
|
||
|
||
private static string GetString(JsonElement element, string property)
|
||
{
|
||
return element.TryGetProperty(property, out var value) ? value.ToString() : "";
|
||
}
|
||
|
||
private static bool GetBool(JsonElement element, string property)
|
||
{
|
||
return element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.True;
|
||
}
|
||
|
||
private static double Round(double value, int digits)
|
||
{
|
||
return Math.Round(value, digits, MidpointRounding.AwayFromZero);
|
||
}
|
||
|
||
private static string ColumnName(int column)
|
||
{
|
||
var name = "";
|
||
while (column > 0)
|
||
{
|
||
column--;
|
||
name = (char)('A' + column % 26) + name;
|
||
column /= 26;
|
||
}
|
||
|
||
return name;
|
||
}
|
||
|
||
private static string EscapeXml(string value)
|
||
{
|
||
return value
|
||
.Replace("&", "&", StringComparison.Ordinal)
|
||
.Replace("<", "<", StringComparison.Ordinal)
|
||
.Replace(">", ">", StringComparison.Ordinal)
|
||
.Replace("\"", """, StringComparison.Ordinal)
|
||
.Replace("'", "'", StringComparison.Ordinal);
|
||
}
|
||
|
||
private static string TrimSheetName(string name)
|
||
{
|
||
return name.Length <= 31 ? name : name[..31];
|
||
}
|
||
|
||
private static string SanitizeFileName(string value)
|
||
{
|
||
var invalid = Path.GetInvalidFileNameChars();
|
||
return new string(value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());
|
||
}
|
||
|
||
private sealed record WorksheetData(string Name, IReadOnlyList<IReadOnlyList<object?>> Rows);
|
||
}
|