d72b583319
[XP.ReportEngine] 模型与配置扩展:ReportOutputFormat 枚举:新增 Excel=1, Csv=2, Txt=3(显式序号,向后兼容);新增多格式相关配置加载。
197 lines
8.6 KiB
C#
197 lines
8.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
|
|
namespace XP.ReportEngine.Services
|
|
{
|
|
/// <summary>
|
|
/// Excel 工作表名称规整器 | Excel worksheet name sanitizer
|
|
/// 为 ExcelReportGenerator 提供工作表名称规整逻辑(需求 11.4)
|
|
/// Provides worksheet name sanitization logic for ExcelReportGenerator (Requirement 11.4)
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Excel 工作表名规则 | Excel worksheet name rules:
|
|
/// - 长度上限 31 个字符 | Maximum length is 31 characters
|
|
/// - 禁止字符:: \ / ? * [ ] | Illegal characters: : \ / ? * [ ]
|
|
/// - 不能为空 | Cannot be empty
|
|
/// - 工作簿内须唯一(不区分大小写,符合 Excel 语义)| Must be unique within the workbook (case-insensitive, matching Excel semantics)
|
|
///
|
|
/// 设计为静态帮助类,便于 ExcelReportGenerator(任务 7.3)复用。
|
|
/// Designed as a static helper so ExcelReportGenerator (task 7.3) can reuse it.
|
|
/// </remarks>
|
|
internal static class ExcelSheetNameSanitizer
|
|
{
|
|
/// <summary>
|
|
/// Excel 工作表名称长度上限 | Maximum length of an Excel worksheet name
|
|
/// </summary>
|
|
private const int MaxSheetNameLength = 31;
|
|
|
|
/// <summary>
|
|
/// Excel 工作表名称非法字符集合 | Set of illegal characters for Excel worksheet names
|
|
/// : \ / ? * [ ]
|
|
/// </summary>
|
|
private static readonly char[] IllegalChars = { ':', '\\', '/', '?', '*', '[', ']' };
|
|
|
|
/// <summary>
|
|
/// 规整工作表名称为合法且工作簿内唯一的名称,并将结果记入 <paramref name="usedNames"/>。
|
|
/// Sanitizes a worksheet name into a legal, workbook-unique name and records the result into <paramref name="usedNames"/>.
|
|
/// </summary>
|
|
/// <param name="processorType">
|
|
/// 名称来源,优先使用处理器类型;为空则使用 Group_{序号}。
|
|
/// Name source; prefer the processor type. If empty, falls back to Group_{index}.
|
|
/// </param>
|
|
/// <param name="groupIndex">
|
|
/// 分组序号,当 <paramref name="processorType"/> 为空时用于生成默认名称。
|
|
/// Group index, used to build the default name when <paramref name="processorType"/> is empty.
|
|
/// </param>
|
|
/// <param name="usedNames">
|
|
/// 工作簿内已使用的名称集合(不区分大小写),用于去重。方法会将最终选定的名称加入该集合。
|
|
/// The set of names already used in the workbook (case-insensitive), used for deduplication.
|
|
/// The method adds the finally chosen name into this set.
|
|
/// </param>
|
|
/// <returns>合法且唯一的工作表名称 | A legal and unique worksheet name</returns>
|
|
public static string SanitizeSheetName(string processorType, int groupIndex, ISet<string> usedNames)
|
|
{
|
|
if (usedNames == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(usedNames));
|
|
}
|
|
|
|
// 1. 确定名称来源:优先 ProcessorType,为空用 Group_{序号}
|
|
// Determine name source: prefer ProcessorType, fall back to Group_{index}
|
|
string source = string.IsNullOrWhiteSpace(processorType)
|
|
? $"Group_{groupIndex}"
|
|
: processorType.Trim();
|
|
|
|
// 2. 替换非法字符为 '_'
|
|
// Replace illegal characters with '_'
|
|
string sanitized = ReplaceIllegalChars(source);
|
|
|
|
// 若替换后为空白(例如全为空白字符),回退到默认名
|
|
// If it becomes blank after replacement (e.g. all whitespace), fall back to the default name
|
|
if (string.IsNullOrWhiteSpace(sanitized))
|
|
{
|
|
sanitized = ReplaceIllegalChars($"Group_{groupIndex}");
|
|
}
|
|
|
|
// 3. 截断至 31 字符
|
|
// Truncate to 31 characters
|
|
sanitized = Truncate(sanitized, MaxSheetNameLength);
|
|
|
|
// 4. 去重:为空或重复时追加序号 _{n},并保证总长 <= 31(必要时截断前缀)
|
|
// Deduplicate: if empty or duplicate, append suffix _{n} while keeping total length <= 31 (truncate prefix as needed)
|
|
string unique = EnsureUnique(sanitized, usedNames);
|
|
|
|
// 记录最终名称 | Record the final name
|
|
usedNames.Add(unique);
|
|
return unique;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将名称中的 Excel 非法字符替换为下划线 | Replaces Excel-illegal characters in the name with underscores
|
|
/// </summary>
|
|
private static string ReplaceIllegalChars(string name)
|
|
{
|
|
var builder = new StringBuilder(name.Length);
|
|
foreach (char c in name)
|
|
{
|
|
builder.Append(IsIllegal(c) ? '_' : c);
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断字符是否为 Excel 工作表名非法字符 | Determines whether a character is illegal in an Excel worksheet name
|
|
/// </summary>
|
|
private static bool IsIllegal(char c)
|
|
{
|
|
foreach (char illegal in IllegalChars)
|
|
{
|
|
if (c == illegal)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将字符串截断至指定最大长度 | Truncates a string to the specified maximum length
|
|
/// </summary>
|
|
private static string Truncate(string value, int maxLength)
|
|
{
|
|
if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
|
|
{
|
|
return value ?? string.Empty;
|
|
}
|
|
|
|
return value.Substring(0, maxLength);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 保证名称在工作簿内唯一(不区分大小写)。为空或重复时追加去重序号 _{n},
|
|
/// 并保证总长不超过 31 个字符(必要时截断前缀)。
|
|
/// Ensures the name is unique within the workbook (case-insensitive). When empty or duplicate,
|
|
/// appends a dedup suffix _{n} while keeping the total length within 31 characters (truncating the prefix as needed).
|
|
/// </summary>
|
|
private static string EnsureUnique(string baseName, ISet<string> usedNames)
|
|
{
|
|
// 若名称非空且不冲突,直接使用 | If the name is non-empty and does not conflict, use it directly
|
|
if (!string.IsNullOrEmpty(baseName) && !IsUsed(baseName, usedNames))
|
|
{
|
|
return baseName;
|
|
}
|
|
|
|
// 名称为空或冲突:追加去重序号 | Name empty or conflicting: append a dedup suffix
|
|
for (int n = 1; n <= int.MaxValue; n++)
|
|
{
|
|
string suffix = "_" + n.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
|
|
// 计算可用于前缀的最大长度,保证 前缀 + 后缀 <= 31
|
|
// Compute the max prefix length so that prefix + suffix <= 31
|
|
int allowedPrefixLength = MaxSheetNameLength - suffix.Length;
|
|
if (allowedPrefixLength < 0)
|
|
{
|
|
allowedPrefixLength = 0;
|
|
}
|
|
|
|
string prefix = Truncate(baseName, allowedPrefixLength);
|
|
|
|
// 若前缀为空(例如 baseName 原本为空),仅使用去重序号本身作为候选名
|
|
// If prefix is empty (e.g. baseName was empty), use the dedup suffix itself as the candidate
|
|
string candidate = prefix.Length == 0
|
|
? Truncate("Sheet" + suffix, MaxSheetNameLength)
|
|
: prefix + suffix;
|
|
|
|
if (!IsUsed(candidate, usedNames))
|
|
{
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
// 理论上不可达 | Practically unreachable
|
|
throw new InvalidOperationException(
|
|
"无法为工作表生成唯一名称 | Unable to generate a unique worksheet name");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断名称是否已被使用(不区分大小写,符合 Excel 语义)
|
|
/// Determines whether a name has already been used (case-insensitive, matching Excel semantics)
|
|
/// </summary>
|
|
private static bool IsUsed(string name, ISet<string> usedNames)
|
|
{
|
|
foreach (string used in usedNames)
|
|
{
|
|
if (string.Equals(used, name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|