using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using TyphoonGUIStyle; using TyphoonIniConfig; using UnityEditor; using UnityEngine; using Random = UnityEngine.Random; using StringBuilder = System.Text.StringBuilder; namespace TyphoonFontTool { public class GroupingWindow : EditorWindow { private static Dictionary _inputModeGUIBinding = new Dictionary() { { InputMode.Characters, ("字符", "输入格式如:1234567890!@#$abcdefg汉字") }, { InputMode.HexCodesRange, ("码点范围(用,分隔)", "输入格式如:00,20-7E,A5,5EA6-5EA7") }, { InputMode.File, ("文本文件", "输入格式如:常用汉字.txt,常用日文.txt") } }; private static Dictionary _characterMap = new Dictionary(); private static int _missPage = 0; private static string GetCharacter(int code) { if (_characterMap.TryGetValue(code, out var match)) { return match; } var result = char.ConvertFromUtf32(code); _characterMap.Add(code, result); return result; } private Dictionary _groupRendererRects = new Dictionary(); public GUIStyle _characterStyle = null; public GUIStyle CharacterStyle { get { if (_characterStyle == null) { _characterStyle = new GUIStyle(GUI.skin.textArea); } return _characterStyle; } } private const string INT_SELECT_RULE = "INT_SELECT_RULE"; private static int LEFT_LAYOUT_WIDTH = 320; private static int MIDDLE_LAYOUT_WIDTH = 400; private static List _rules = new List(); //分组规则 private static GroupingRule _select = null; //当前选择 private static List _fontSource = new List(); private static Vector2 _scrollLeft; private static SelectFontSourceData _selectFontSourceData; private static Vector2 _scrollOutPut; private static GroupingRule.CharacterData _deleteData = null; private struct SelectFontSourceData { public GroupingRule Rule; public int SelectIndex; } [MenuItem("Typhoon/FontTool/字符分组")] private static void Open() { var win = GetWindow(); win.minSize = new Vector2(1200, 600); win.titleContent = new GUIContent("字符分组"); win.Show(); } private static INIConfig _iniConfig; private static INIConfig IniConfig { get { if (_iniConfig == null) { _iniConfig = new INIConfig(_Editor.INIConfigPath); } return _iniConfig; } } private void OnEnable() { ReloadRules(); ReloadFontSource(); LoadCacheSelectRule(); } //重载GroupingRule private static void ReloadRules() { _rules.Clear(); var guids = AssetDatabase.FindAssets($"t:{typeof(GroupingRule).FullName}", new[] { "Assets" }); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null) { _rules.Add(asset); } } } //重载FontSourceData private static void ReloadFontSource() { _fontSource.Clear(); var guids = AssetDatabase.FindAssets($"t:{typeof(FontSourceData).FullName}", new[] { "Assets" }); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null) { _fontSource.Add(asset); } } } private void OnFocus() { ReloadRules(); ReloadFontSource(); LoadCacheSelectRule(); Repaint(); } private void OnGUI() { //分组模式 var rect = new Rect(Vector2.zero, position.size); var leftLayout = rect; leftLayout.width = LEFT_LAYOUT_WIDTH; DrawLeftLayoutGUI(leftLayout); var line = leftLayout; line.width = 1; line.x = leftLayout.xMax; EditorGUI.DrawRect(line, Color.black); var middleLayout = line; middleLayout.x = line.xMax; middleLayout.width = MIDDLE_LAYOUT_WIDTH; DrawMiddleLayoutGUI(middleLayout); line = middleLayout; line.width = 1; line.x = middleLayout.xMax; EditorGUI.DrawRect(line, Color.black); var rightLayout = line; rightLayout.x = line.xMax; rightLayout.width = rect.xMax - rightLayout.x; DrawRightLayoutGUI(rightLayout); Repaint(); } //绘制左侧布局 private void DrawLeftLayoutGUI(Rect area) { var rect = area; var center = rect.center; var draw = rect; draw.width -= 8; draw.height -= 8; draw.center = center; //绘制分组规则 GUILayout.BeginArea(draw); GUILayout.Label("基础设置", Styles.TitleBar); GUILayout.Label($"发现 {_rules.Count} 个分组配置"); GUILayout.Space(3); var txt = "请选择分组规则"; if (_select != null) { txt = _select.name; } if (GUILayout.Button(txt, "PopUp")) { ClearFocus(); var menu = new GenericMenu(); menu.AddItem(new GUIContent("+新建规则"), false, () => { var rule = CreateRule(); ReloadRules(); SelectRule(rule); }); menu.AddItem(new GUIContent(""), false, null); //绘制规则清单 foreach (var rule in _rules) { if (rule == null) { continue; } var tog = rule == _select; menu.AddItem(new GUIContent($"{rule.name}"), tog, () => { SelectRule(rule); }); } menu.ShowAsContext(); } if (_rules.Count <= 0) { if (GUILayout.Button("导入默认资源", Styles.BtnGreen, GUILayout.Height(28))) { _Editor.GenerateNotoSansSourceData(); ReloadRules(); ReloadFontSource(); LoadCacheSelectRule(); } } if (_select != null) { var temEnable = GUI.enabled; GUI.enabled = false; EditorGUILayout.ObjectField(_select, typeof(GroupingRule)); GUI.enabled = temEnable; } GUILayout.Space(10); if (_select != null) { //绘制分组规则 GroupingRuleGUI(_select); } GUILayout.EndArea(); } //绘制右侧布局 private void DrawMiddleLayoutGUI(Rect area) { var rect = area; var center = rect.center; //绘制详情 var draw = rect; draw.width -= 8; draw.height -= 8; draw.center = center; if (_select == null) { GUI.Label(draw, "请选择分组配置", Styles.BoldLabel); } else { var codeRect = draw; codeRect.height = 266; GUILayout.BeginArea(draw); var binding = _inputModeGUIBinding[_select.InputMode]; var btnLabel = binding.menuName; var tipsLabel = binding.label; GUILayout.Label("输入设置", Styles.TitleBar); GUILayout.BeginHorizontal(); GUILayout.Label("模式:", GUILayout.Width(80)); if (GUILayout.Button(btnLabel, "PopUp")) { ClearFocus(); var modes = new[] { InputMode.Characters, InputMode.HexCodesRange, InputMode.File }; var menu = new GenericMenu(); foreach (var mode in modes) { menu.AddItem(new GUIContent(_inputModeGUIBinding[mode].menuName), false, () => { _select.InputMode = mode; _select.Save(); ClearFocus(); }); } menu.ShowAsContext(); } GUILayout.EndHorizontal(); GUILayout.Space(3); GUILayout.Label(new GUIContent(tipsLabel, Styles.IconInfo), Styles.HelpBox); GUILayout.Space(3); switch (_select.InputMode) { case InputMode.HexCodesRange: DrawInputHexCodeStringGUI(draw.width); break; case InputMode.Characters: DrawInputCharactersGUI(draw.width); break; case InputMode.File: DrawInputFileGUI(); break; default: throw new ArgumentOutOfRangeException(); } GUILayout.Space(3); if (GUILayout.Button("分析&输出结果", Styles.BtnGreen, GUILayout.Height(36))) { ParserToCharacterGroup(_select); ClearFocus(); } GUILayout.EndArea(); } } private void DrawRightLayoutGUI(Rect area) { var rect = area; var center = rect.center; //绘制详情 var draw = rect; draw.width -= 8; draw.height -= 8; draw.center = center; GUILayout.BeginArea(draw); if (_select != null) { GUILayout.Label("输出结果", Styles.TitleBar); _scrollOutPut = GUILayout.BeginScrollView(_scrollOutPut); var output = _select.Output; var sb = new StringBuilder(); var startY = _scrollOutPut.y; var endY = _scrollOutPut.y + draw.height; foreach (var item in output) { sb.Clear(); if (item == null || item.FontSource == null) { continue; } GUILayout.BeginHorizontal(); if (item.FontSource.Font != null) { GUILayout.BeginHorizontal(); GUILayout.Label($"{item.FontSource.Font.name} ({item.Codes.Count})", Styles.BoldLabel); var path = item.GetOutputFile(_select); var asset = AssetDatabase.LoadAssetAtPath(path); EditorGUILayout.ObjectField(asset, typeof(TextAsset), GUILayout.Width(140)); GUILayout.EndHorizontal(); } else { GUILayout.Label($"{item.FontSource.name}(字体missing)", Styles.BoldLabel); } GUILayout.EndHorizontal(); if (item.FontSource.Font == null) { GUILayout.Label("字体丢失,无法预览"); GUILayout.Space(10); continue; } var style = CharacterStyle; var height = style.CalcHeight(item.GetPreviewGUIContent(style), draw.width); height = Mathf.Max(height, 50); height = height > 400 ? 400 : height; var hasCode = item.GetHashCode(); var show = false; if (_groupRendererRects.TryGetValue(hasCode, out var match)) { if ((match.yMin > startY && match.yMin < endY) || (match.yMax > startY && match.yMax < endY)) { show = true; } // Debug.Log($"{item.FontSource.name} {match} [{startY},{endY}] 显示:{show}"); } //缓存区域范围,优化显示 if (show) { EditorGUILayout.TextArea(item.CharacterString, style, GUILayout.Height(height)); } else { EditorGUILayout.TextArea("", style, GUILayout.Height(height)); } if (Event.current.type == EventType.Repaint) { _groupRendererRects[hasCode] = GUILayoutUtility.GetLastRect(); } GUILayout.Space(10); } GUILayout.Space(5); //缺失字符 if (_select.MissingCode != null && _select.MissingCode.Count > 0) { GUILayout.Label( new GUIContent($"缺失字符,共:{_select.MissingCode.Count} 分页数:{_select.MissCodeGUIContent.Length}", Styles.IconWarning), Styles.HelpBox); if (_select.MissCodeGUIContent != null && _select.MissCodeGUIContent.Length > 0) { GUILayout.BeginHorizontal(); var page = _missPage + 1; if (GUILayout.Button("上一页", Styles.Square.Btn, GUILayout.Height(22))) { page -= 1; } page = EditorGUILayout.IntField(page, GUILayout.Width(120), GUILayout.Height(22)); if (GUILayout.Button("下一页", Styles.Square.Btn)) { page += 1; } GUILayout.EndHorizontal(); _missPage = Mathf.Clamp(page - 1, 0, _select.MissCodeGUIContent.Length - 1); if (_missPage < _select.MissCodeGUIContent.Length && _missPage >= 0) { var content = _select.MissCodeGUIContent[_missPage]; EditorGUILayout.TextArea(content, CharacterStyle); } } } GUILayout.Space(20); GUILayout.EndScrollView(); } GUILayout.EndArea(); } private void GroupingRuleGUI(GroupingRule rule) { GUILayout.BeginHorizontal(); GUILayout.Label("模式:"); if (GUILayout.Button($"{rule.Mode}", "PopUp", GUILayout.Height(24))) { ClearFocus(); var menu = new GenericMenu(); menu.AddItem(new GUIContent("漏斗模式(层层过滤)"), false, () => { rule.Mode = GroupingMode.Funnel; rule.Save(); }); menu.AddItem(new GUIContent("普通模式"), false, () => { rule.Mode = GroupingMode.Normal; rule.Save(); }); menu.ShowAsContext(); } GUILayout.EndHorizontal(); GUILayout.Label($"字体资源", Styles.TitleBar); var last = GUILayoutUtility.GetLastRect(); var btnImportRect = last; var center = btnImportRect.center; var draw = btnImportRect; draw.height -= 4; draw.width = 160; draw.center = center; draw.x = btnImportRect.xMax - draw.width; draw.x -= 2; if (GUI.Button(draw, "导入默认资源", Styles.BtnBlue)) { _Editor.GenerateNotoSansSourceData(); ReloadRules(); ReloadFontSource(); LoadCacheSelectRule(); } _scrollLeft = GUILayout.BeginScrollView(_scrollLeft); var sourceList = rule.FontSourceList.ToList(); var index = 0; foreach (var item in sourceList) { GUILayout.BeginHorizontal(); GUILayout.Label("", GUILayout.Height(24)); last = GUILayoutUtility.GetLastRect(); var selectStatus = _select == _selectFontSourceData.Rule && index == _selectFontSourceData.SelectIndex; if (selectStatus) { var outline = last; outline.width += 2; outline.height += 2; outline.center = last.center; EditorGUI.DrawRect(outline, Styles.ColorSelected); } EditorGUI.DrawRect(last, (index % 2 == 0 ? Color.black * 0.1f : Color.white * 0.1f)); var labRect = last; labRect.width -= 80; var missing = item == null; var col = GUI.color; GUI.color = missing ? Color.red : col; if (GUI.Button(labRect, $"{(item == null ? "(missing)" : item.name)}", Styles.BoldLabel)) { SelectFontSource(_select, index); if (selectStatus && item != null) { Selection.activeObject = item; _Editor.Ping(item); } } GUI.color = col; var btn = last; btn.width = 24; btn.x = last.xMax - btn.width; center = btn.center; draw = btn; draw.width -= 4; draw.height -= 4; draw.center = center; if (GUI.Button(draw, "≡", Styles.Btn)) { var currentIndex = index; var menu = new GenericMenu(); menu.AddItem(new GUIContent("删除"), false, () => { Delete(currentIndex); }); menu.AddItem(new GUIContent(""), false, null); menu.AddItem(new GUIContent("上移"), false, () => { MoveUp(currentIndex); }); menu.AddItem(new GUIContent("下移"), false, () => { MoveDown(currentIndex); }); menu.AddItem(new GUIContent(""), false, null); menu.AddItem(new GUIContent("移到顶部"), false, () => { MoveTop(currentIndex); }); menu.AddItem(new GUIContent("移到底部"), false, () => { MoveBottom(currentIndex); }); menu.ShowAsContext(); } GUILayout.EndHorizontal(); index += 1; } GUILayout.Space(3); GUILayout.BeginHorizontal(); if (GUILayout.Button($"+新增", Styles.BtnGreen, GUILayout.Height(24), GUILayout.Width(66))) { ClearFocus(); var menu = new GenericMenu(); var sources = _fontSource; var hashSet = rule.FontSourceList.ToHashSet(); foreach (var item in sources) { var tog = hashSet.Contains(item); menu.AddItem(new GUIContent($"{item.name}"), tog, () => { var set = rule.FontSourceList.ToHashSet(); set.Add(item); rule.FontSourceList = set.ToList(); rule.Save(); }); } menu.AddItem(new GUIContent("解析新字体文件"), false, () => { FontCharactersParser.Open(); }); menu.ShowAsContext(); } GUILayout.EndHorizontal(); GUILayout.EndScrollView(); } private void LoadCacheSelectRule() { var cacheRule = IniConfig.GetString(INT_SELECT_RULE); var match = _rules.LastOrDefault(e => e.name == cacheRule); _select = match; } private void SelectRule(GroupingRule rule, bool cacheIni = true) { _select = rule; if (cacheIni) { IniConfig.Set(INT_SELECT_RULE, _select.name); } } private static GroupingRule CreateRule() { var path = $"Assets/Typhoon_Gen/FontTool/Editor/GroupingRules/分组规则_{DateTime.Now.ToString("yyMMdd-HHmmss")}-{Random.Range(100, 999)}.asset"; var asset = CreateInstance(); _Editor.CreateFileDirectoryIfNotExists(path); AssetDatabase.CreateAsset(asset, path); _Editor.Ping(asset); return asset; } private void SelectFontSource(GroupingRule rule, int index) { _selectFontSourceData.Rule = rule; _selectFontSourceData.SelectIndex = index; } private void Delete(int currentIndex) { if (_select == null) { return; } var list = _select.FontSourceList; if (currentIndex >= 0 && currentIndex < list.Count) { list.RemoveAt(currentIndex); _select.Save(); } } private void MoveUp(int currentIndex) { if (_select == null) { return; } var list = _select.FontSourceList; var last = currentIndex - 1; if (last >= 0 && last < list.Count && currentIndex >= 0 && currentIndex < list.Count) { (list[currentIndex], list[last]) = (list[last], list[currentIndex]); SelectFontSource(_select, last); _select.Save(); } } private void MoveDown(int currentIndex) { if (_select == null) { return; } var list = _select.FontSourceList; var next = currentIndex + 1; if (next >= 0 && next < list.Count && currentIndex >= 0 && currentIndex < list.Count) { (list[currentIndex], list[next]) = (list[next], list[currentIndex]); SelectFontSource(_select, next); _select.Save(); } } private void MoveTop(int currentIndex) { if (_select == null) { return; } var list = _select.FontSourceList; if (currentIndex >= 0 && currentIndex < list.Count) { var tem = list[currentIndex]; list.RemoveAt(currentIndex); list.Insert(0, tem); SelectFontSource(_select, 0); _select.Save(); } } private void MoveBottom(int currentIndex) { if (_select == null) { return; } var list = _select.FontSourceList; if (currentIndex >= 0 && currentIndex < list.Count) { var tem = list[currentIndex]; list.RemoveAt(currentIndex); list.Add(tem); SelectFontSource(_select, list.Count - 1); _select.Save(); } } //中间-码点范围 private void DrawInputHexCodeStringGUI(float width) { GUILayout.Label("码点范围", Styles.TitleBar); _select.Scroll = GUILayout.BeginScrollView(_select.Scroll, GUILayout.ExpandHeight(false)); var style = EditorStyles.textArea; var height = Mathf.Max(style.CalcHeight(new GUIContent(_select.InputHexCodeRange), width - 16), 120); _select.InputHexCodeRange = EditorGUILayout.TextArea(_select.InputHexCodeRange, style, GUILayout.Width(width - 16), GUILayout.Height(height)); GUILayout.EndScrollView(); } //中间-字符 private void DrawInputCharactersGUI(float width) { GUILayout.Label("字符", Styles.TitleBar); var last = GUILayoutUtility.GetLastRect(); var btnLoadCurrentRect = last; var center = last.center; btnLoadCurrentRect.width = 120; btnLoadCurrentRect.height -= 4; btnLoadCurrentRect.center = center; btnLoadCurrentRect.x = last.xMax - btnLoadCurrentRect.width; btnLoadCurrentRect.x -= 4; if (GUI.Button(btnLoadCurrentRect, "读取当前字符集")) { ClearFocus(); var codes = new HashSet(); var output = _select.Output; foreach (var data in output) { foreach (var code in data.Codes) { codes.Add(code); } } if (_select.MissingCode != null) { foreach (var code in _select.MissingCode) { codes.Add(code); } } var sb = new StringBuilder(); foreach (var code in codes) { sb.Append(char.ConvertFromUtf32(code)); } _select.InputCharacterString = sb.ToString(); _select.Save(); } _select.Scroll = GUILayout.BeginScrollView(_select.Scroll, GUILayout.ExpandHeight(false)); var tem = _select.InputCharacterString; var style = EditorStyles.textArea; var height = Mathf.Max(style.CalcHeight(new GUIContent(_select.InputCharacterString), width - 16), 120); _select.InputCharacterString = EditorGUILayout.TextArea(_select.InputCharacterString, style, GUILayout.Width(width - 16), GUILayout.Height(height)); if (tem != _select.InputCharacterString) { EditorUtility.SetDirty(_select); } GUILayout.EndScrollView(); } //中间-字符文件 private void DrawInputFileGUI() { GUILayout.Label("文本文件", Styles.TitleBar); _select.Scroll = GUILayout.BeginScrollView(_select.Scroll, GUILayout.ExpandHeight(false)); if (_select.InputFiles == null) { _select.InputFiles = new List(); _select.Save(); } if (_select.InputFiles.Count <= 0) { _select.InputFiles.Add(null); _select.Save(); } var files = _select.InputFiles; var deleteIndex = -1; for (var i = 0; i < files.Count; i++) { var file = files[i]; GUILayout.BeginHorizontal(); files[i] = EditorGUILayout.ObjectField(file, typeof(TextAsset), GUILayout.Height(22)) as TextAsset; if (GUILayout.Button("移除", GUILayout.Width(44), GUILayout.Height(22))) { deleteIndex = i; ClearFocus(); } GUILayout.EndHorizontal(); } if (deleteIndex >= 0) { _select.InputFiles.RemoveAt(deleteIndex); } GUILayout.Space(3); if (GUILayout.Button("+新增 File", GUILayout.Width(120), GUILayout.Height(24))) { ClearFocus(); _select.InputFiles.Add(null); _select.Save(); } GUILayout.EndScrollView(); } private void OnDestroy() { if (_rules != null) { foreach (var rule in _rules) { if (EditorUtility.IsDirty(rule)) { AssetDatabase.SaveAssets(); } } } } private void ClearFocus() { GUI.FocusControl(""); } public static void ParserToCharacterGroup(GroupingRule rule) { //判断当前输入机制 switch (rule.InputMode) { case InputMode.HexCodesRange: //不处理 break; case InputMode.Characters: { //生成码点范围 rule.InputHexCodeRange = CharactersToHexCodeRangeString(rule.InputCharacterString); rule.Save(); } break; case InputMode.File: { var sb = new StringBuilder(); foreach (var file in rule.InputFiles) { if (file != null) { sb.Append(file.text); } } rule.InputHexCodeRange = CharactersToHexCodeRangeString(sb.ToString()); rule.Save(); } break; } try { var codes = new HashSet(); var final = rule.InputHexCodeRange.Trim().Replace("\n", "").Replace("\r", "").Replace("\t", ""); if (!string.IsNullOrEmpty(final)) { codes = new HashSet(); var range = final.Split(',').Where(e => !string.IsNullOrWhiteSpace(e)).Select(e => { var items = e.Split('-'); var from = items[0]; var to = from; if (items.Length > 1) { to = items[1]; } var fromValue = Convert.ToInt32(from, 16); var toValue = Convert.ToInt32(to, 16); return new { fromValue, toValue }; }); foreach (var item in range) { for (int i = item.fromValue; i <= item.toValue; i++) { codes.Add(i); } } } //拆分 var fontSourceList = rule.FontSourceList; var dic = new Dictionary>(); foreach (var data in fontSourceList) { if (!dic.ContainsKey(data)) { dic.Add(data, new HashSet()); } } var validFonts = fontSourceList.Where(e => e != null).Distinct().ToList(); var map = validFonts.ToDictionary(e => e, v => new HashSet()); var used = new HashSet(); foreach (var source in validFonts) { foreach (var code in codes) { if (source.IsContains(code)) { map[source].Add(code); used.Add(code); } } } var miss = codes.ToList().Except(used).ToList(); switch (rule.Mode) { case GroupingMode.Funnel: //漏斗模式,一层层过滤 { var filter = new HashSet(); foreach (var source in validFonts) { var result = map[source].Where(e => !filter.Contains(e)).ToHashSet(); map[source] = result; foreach (var element in result) { filter.Add(element); } } } break; } rule.Output = new List(); foreach (var kv in map) { rule.Output.Add(new GroupingRule.CodeGroupData(kv.Key, kv.Value.ToList())); } rule.MissingCode = miss.ToList(); //生成字符文件 var output = rule.Output; foreach (var data in output) { if (data.FontSource == null && data.FontSource.Font == null) { continue; } var saveFile = data.GetOutputFile(rule); _Editor.CreateFileDirectoryIfNotExists(saveFile); File.WriteAllText(saveFile, data.CharacterString); _Editor.Ping(saveFile); } AssetDatabase.Refresh(); } catch (Exception e) { throw new Exception($"解析失败:{e.ToString()}"); } } private static string CharactersToHexCodeRangeString(string input) { var characters = new HashSet(); //提取字符串的单个字符 var enumerator = StringInfo.GetTextElementEnumerator(input); while (enumerator.MoveNext()) { var character = enumerator.GetTextElement(); characters.Add(character); } var codes = new HashSet(); //字符转码点值 foreach (var character in characters) { //字符转码点值 int unicodeValue = Char.ConvertToUtf32(character, 0); codes.Add(unicodeValue); } // Debug.Log(characters.Count); //码点值排序,生成区间 var list = codes.ToList(); list.Sort(); if (list.Count > 0) { var range = new List>(); var stack = new Stack(); stack.Push(list[0]); //划分码点值区间 foreach (var code in list) { if (stack.Peek() == code) { continue; } if (code - stack.Peek() == 1) { stack.Push(code); continue; } //整合stack,生成范围,填充到下一个stack range.Add(stack.ToList()); stack = new Stack(); stack.Push(code); } if (stack.Count > 0) { range.Add(stack.ToList()); } var sb = new StringBuilder(); var isFirst = true; //生成码点范围 foreach (var item in range) { item.Sort(); if (!isFirst) { sb.Append(","); } var first = item.First(); var last = item.Last(); var hexStart = first.ToString("X").ToUpper().PadLeft(2, '0'); var hexEnd = last.ToString("X").ToUpper().PadLeft(2, '0'); if (hexStart == hexEnd) { sb.Append(hexStart); } else { sb.Append($"{hexStart}-{hexEnd}"); } isFirst = false; } return sb.ToString(); } return ""; } } }