Files
FootwearTest/FootwearTest/Services/ExcelReportService.cs
GukSang.Jin 0a908d85df 更新2026
2026-05-30 15:04:42 +08:00

563 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 gm180 应在 (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"), "gC1 消耗水量"));
rows.Add(Row($"{prefix} m6", GetDouble(trial, "M6"), "gC2 蒸发水量"));
rows.Add(Row($"{prefix} m7", GetDouble(trial, "M7"), "gC3 蒸发水量"));
rows.Add(Row($"{prefix} m8", GetDouble(trial, "M8"), "gC2/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"), "kJ180 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("&", "&amp;", StringComparison.Ordinal)
.Replace("<", "&lt;", StringComparison.Ordinal)
.Replace(">", "&gt;", StringComparison.Ordinal)
.Replace("\"", "&quot;", StringComparison.Ordinal)
.Replace("'", "&apos;", 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);
}