Глобальный рефакторинг пространств имён

This commit is contained in:
2025-10-02 13:29:30 +04:00
parent d23d81fb53
commit 2fa94851cb
3176 changed files with 27146 additions and 30584 deletions
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json.Linq;
using Pf2e.Scrapper.Helpers;
using Pf2e.Scrapper.Importers;
using Pf2e.Scrapper.Processors.Definitions;
namespace Pf2e.Scrapper.SchemaBuilder;
internal abstract class AbstractSchemaBuildProcessor : IProcessor
{
/// <summary>
/// Связаный компендиум
/// </summary>
protected abstract AbstractCompendium Compendium { get; }
/// <inheritdoc cref="ProcessProgress"/>
protected abstract ProcessProgress Progress { get; }
/// <summary>
/// Определитель типов данных.
/// </summary>
protected abstract TypeResolver Resolver { get; }
/// <summary>
/// Путь к каталогу для сохранения вывода.
/// </summary>
protected abstract string SavePath { get; }
/// <summary>
/// Набор схем данных.
/// </summary>
protected Dictionary<string, SchemaNode> Schemas { get; } = [];
/// <summary>
/// Получить список названий элементов структуры данных на основе вводимого элемента.
/// </summary>
/// <param name="item">Вводимый элемент</param>
/// <returns>Коллекция названий элементов структуры данных/</returns>
protected abstract ICollection<string> GetSchemaNodes(AbstractCompendiumItem item);
/// <summary>
/// Построение схемы данных.
/// </summary>
public void Process()
{
Progress.Local.Reset(Compendium.Count());
foreach ((AbstractCompendiumItem reference, JObject json) in Compendium.Iterate())
{
Progress.Local.Start(reference.ToString());
foreach (string nodeName in GetSchemaNodes(reference))
{
if (!Schemas.TryGetValue(nodeName, out SchemaNode? node))
{
node = new() { Path = nodeName, Resolver = Resolver };
Schemas.Add(nodeName, node);
}
node.AddValue(json);
}
Progress.Local.End();
}
Progress.Local.Complete();
}
/// <summary>
/// Сохранение постренных схем данных.
/// </summary>
public void Save()
{
string targetDir = SavePath;
foreach (KeyValuePair<string, SchemaNode> kvp in Schemas)
{
SaveSchema(targetDir, kvp.Key, kvp.Value);
}
}
/// <summary>
/// Сохранение отдельной схемы данных.
/// </summary>
/// <param name="dir">Целевой каталог.</param>
/// <param name="name">Название схемы.</param>
/// <param name="schema">Сохраняемая схема.</param>
protected void SaveSchema(string dir, string name, SchemaNode schema)
{
char[] invalids = Path.GetInvalidFileNameChars();
string fileName = string.Join(
"_",
schema.Path.Split(invalids, StringSplitOptions.RemoveEmptyEntries)
)
.TrimEnd('.');
string path = Path.Combine(dir, fileName + ".yaml");
string? dirPath = Path.GetDirectoryName(path);
if (dirPath is not null && !Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
using StreamWriter writer = new(path);
schema.Beautify();
writer.WriteLine(
SchemaNode.FormatYaml(
new Dictionary<string, string>()
{
["name"] = name,
["values"] = schema.Values.ToString(),
},
schema.Types[0].GetYamlData()
)
);
}
}
+240
View File
@@ -0,0 +1,240 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Pf2e.Scrapper.SchemaBuilder.Types;
namespace Pf2e.Scrapper.SchemaBuilder;
/// <summary>
/// Элемент схемы данных.
/// </summary>
/// <example>
/// Пример использования:
/// <code>
/// SchemaNode schema = new (); // Создание схемы данных
/// data = JObject.Parse(reader.ReadToEnd()); // Загрузка данных
/// schema.AddValue(data); // Добавление данных
/// </code>
/// </example>
internal class SchemaNode
{
/// <summary>
/// Отображаемое имя элемента.
/// </summary>
public readonly string Name = "";
/// <summary>
/// Путь к элементу.
/// </summary>
public string Path = "";
/// <summary>
/// Родительский элемент.
/// </summary>
public readonly SchemaNode? Parent;
/// <summary>
/// Указание, является ли элемент корневым.
/// </summary>
public readonly bool Root;
/// <summary>
/// Корневой элемент схемы данных.
/// </summary>
public SchemaNode RootNode => Root ? this : Parent!.RootNode;
/// <summary>
/// Типы данных, использующиеся в элементе.
/// </summary>
public readonly List<AbstractSchemaType> Types = [];
/// <summary>
/// Указание, может ли этот элемент отсутсвовать в структуре данных.
/// </summary>
public bool IsNullable;
/// <summary>
/// Объект для определения типа поля в схеме данных.
/// </summary>
public TypeResolver Resolver = new();
/// <summary>
/// Количество значений, добавленных в тип.
/// </summary>
public int Values { get; protected set; }
/// <summary>
/// Указание, является ли тип элемента простым.
/// </summary>
public bool IsSimple => Types.Count == 1;
/// <summary>
/// Конструктор корневого элемента.
/// </summary>
public SchemaNode()
{
Root = true;
}
/// <summary>
/// Конструктор элемента схемы данных.
/// </summary>
/// <param name="parent">Родительский элемент.</param>
/// <param name="name">Имя элемента.</param>
public SchemaNode(SchemaNode parent, string name)
{
Parent = parent;
Name = name;
Path = parent.Path + "." + name;
Resolver = parent.Resolver;
}
/// <summary>
/// Добавление нового возможного значения.
/// </summary>
/// <param name="value">Добавляемое значение</param>
public void AddValue(JToken? value)
{
if (value is null || Resolver.TokenType(value) == JTokenType.Null)
{
IsNullable = true;
return;
}
Values++;
foreach (AbstractSchemaType type in Types.Where(type => type.IsCompatibleValue(value)))
{
type.AddValue(value);
return;
}
AbstractSchemaType newType = Resolver.Resolve(this, value);
if (newType.IsCompatibleValue(value))
{
Types.Add(newType);
newType.AddValue(value);
}
else
{
AbstractSchemaType fallbackType = Resolver.Resolve(this, value, true);
Types.Add(fallbackType);
fallbackType.AddValue(value);
}
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public string GetTypeName()
{
if (IsSimple)
{
return Types[0].GetTypeName();
}
List<string> typeNames = [.. Types.Select(t => t.GetTypeName())];
return string.Join(" | ", typeNames);
}
/// <summary>
/// Полное строковое представление элемента.
/// </summary>
/// <returns>Строковое представление элемента.</returns>
public override string ToString()
{
Dictionary<string, string> result = new()
{
["type"] = GetTypeName(),
["path"] = Path,
["is_null"] = IsNullable ? "true" : "false",
["values"] = Values.ToString(),
};
if (IsSimple)
{
return FormatYaml(result, Types[0].GetYamlData());
}
if (Types.Count == 0)
{
return FormatYaml(result, new() { ["is_empty"] = "true" });
}
List<string> types = [];
foreach (AbstractSchemaType type in Types)
{
types.Add(Indent(type.ToString()));
}
return FormatYaml(result, new() { ["types"] = "- " + string.Join("\r\n- ", types) });
}
/// <summary>
/// Подготовка элемента к красивому представлению.
/// </summary>
public void Beautify()
{
foreach (AbstractSchemaType type in Types)
{
type.Beautify();
}
}
/// <summary>
/// Отступ для строкового представления.
/// </summary>
public const string Tab = " ";
/// <summary>
/// Предел количество отображаемых элементов.
/// </summary>
public const int ElementLimit = 20;
/// <summary>
/// Увеличение отступа строки.
/// </summary>
/// <param name="text">Изменяемая строка.</param>
/// <returns>Строка с добавленными отступами.</returns>
public static string Indent(string text)
{
return text.Replace("\r\n", "\r\n" + Tab);
}
/// <summary>
/// Форматирование данных в YAML.
/// </summary>
/// <param name="input">Словарь данных.</param>
/// <returns>Строка форматированных данных</returns>
public static string FormatYaml(params List<Dictionary<string, string>> input)
{
string result = "";
foreach (KeyValuePair<string, string> kvp in MergeDictionaries(input))
{
string val = kvp.Value.Contains("\r\n") ? $"\r\n{Tab}{Indent(kvp.Value)}" : kvp.Value;
result += $"{kvp.Key}: {val}\r\n";
}
return result.Trim();
}
public static Dictionary<string, string> MergeDictionaries(
params List<Dictionary<string, string>> input
)
{
Dictionary<string, string> result = [];
foreach (KeyValuePair<string, string> kvp in input.SelectMany(item => item))
{
result[kvp.Key] = kvp.Value;
}
return result;
}
}
+104
View File
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Pf2e.Scrapper.SchemaBuilder.Types;
namespace Pf2e.Scrapper.SchemaBuilder;
/// <summary>
/// Класс для определения типа поля в схеме данных.
/// </summary>
internal class TypeResolver
{
/// <summary>
/// Проверка соответствия пути.
/// </summary>
/// <param name="node">Элемент, путь которого проверяется.</param>
/// <param name="path">Проверяемый путь. Допускается использование `*` в местах, где допускается любое значение.</param>
/// <returns>`true`, если `node.Path` соответсвует указанному в `path` значению.</returns>
public bool CheckPath(SchemaNode node, string path)
{
if (!path.Contains("*"))
{
return path == node.Path;
}
string[] pathParts = path.Split('.');
string[] nodeParts = node.Path.Split('.');
return pathParts.Length <= nodeParts.Length
&& !pathParts.Where((t, i) => t != "*" && t != nodeParts[i]).Any();
}
/// <summary>
/// Проверка соответствия пути.
/// </summary>
/// <param name="node">Элемент, путь которого проверяется.</param>
/// <param name="paths">Проверяемые пути. Допускается использование `*` в местах, где допускается любое значение.</param>
/// <returns>`true`, если `node.Path` соответсвует хотя бы одному указанному в `paths` значению.</returns>
public bool CheckPath(SchemaNode node, ICollection<string> paths)
{
return paths.Any(path => CheckPath(node, path));
}
/// <summary>
/// Получение типа значения.
/// </summary>
/// <param name="value">Значение.</param>
/// <returns>Типа значения.</returns>
public JTokenType TokenType(JToken? value)
{
return
value is null
|| value.Type is JTokenType.Null or JTokenType.Undefined or JTokenType.None
? JTokenType.Null
: value.Type;
}
/// <summary>
/// Частное определение типа поля.
/// </summary>
/// <param name="node">Поле схемы данных, для которого создаётся тип.</param>
/// <param name="value">Добавляемое значение.</param>
/// <returns>Новый тип для схемы данных. `null`, что бы использовать значение по умолчанию</returns>
protected virtual AbstractSchemaType? CustomResolve(SchemaNode node, JToken? value)
{
return null;
}
/// <summary>
/// Определение типа поля.
/// </summary>
/// <param name="node">Поле схемы данных, для которого создаётся тип.</param>
/// <param name="value">Добавляемое значение.</param>
/// <param name="disableResolve">Добавляемое значение.</param>
/// <returns>Новый тип для схемы данных</returns>
/// <exception cref="ArgumentNullException">
/// Возникает, если у `value` значение идентично `null`
/// (такие значения должны обрабаотываться в `SchemaNode`).
/// </exception>
public AbstractSchemaType Resolve(SchemaNode node, JToken? value, bool disableResolve = false)
{
if (!disableResolve)
{
AbstractSchemaType? customType = this.CustomResolve(node, value);
if (customType is not null)
{
return customType;
}
}
JTokenType type = TokenType(value);
return type == JTokenType.Null
? throw new ArgumentNullException(nameof(value))
: type switch
{
JTokenType.Array => new ArraySchemaType(node),
JTokenType.Object => new ObjectSchemaType(node),
_ => new StaticSchemaType(node, type),
};
}
}
@@ -0,0 +1,65 @@
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Pf2e.Scrapper.SchemaBuilder.Types;
/// <summary>
/// Абстрактный тип представоления данных в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal abstract class AbstractSchemaType(SchemaNode parent)
{
/// <summary>
/// Элемент схемы данных, к которому относится тип.
/// </summary>
public readonly SchemaNode Parent = parent;
/// <summary>
/// Корневой элемент схемы данных.
/// </summary>
public SchemaNode Root => Parent.RootNode;
/// <summary>
/// Объект для определения типа поля в схеме данных.
/// </summary>
public TypeResolver Resolver => Root.Resolver;
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public abstract bool IsCompatibleValue(JToken value);
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public abstract void AddValue(JToken value);
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public abstract void Beautify();
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public abstract string GetTypeName();
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public abstract Dictionary<string, string> GetYamlData();
/// <summary>
/// Полное строковое представление элемента.
/// </summary>
/// <returns>Строковое представление элемента.</returns>
public override string ToString()
{
return SchemaNode.FormatYaml(GetYamlData());
}
}
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Pf2e.Scrapper.SchemaBuilder.Types;
/// <summary>
/// Тип представоления массива в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal class ArraySchemaType(SchemaNode parent) : AbstractSchemaType(parent)
{
/// <summary>
/// Минимальное количество элементов в массиве.
/// </summary>
public int MinCount = int.MaxValue;
/// <summary>
/// Максимальное количество элементов в массиве.
/// </summary>
public int MaxCount = int.MinValue;
/// <summary>
/// Схема элемента массива.
/// </summary>
protected readonly SchemaNode ElementSchema = new(parent, "[]");
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{
return Resolver.TokenType(value) == JTokenType.Array;
}
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
{
if (value.Type != JTokenType.Array)
{
throw new("Value is not an array, did you forget to call IsCompatibleValue?");
}
JArray array = (JArray)value;
MinCount = Math.Min(MinCount, array.Count);
MaxCount = Math.Max(MaxCount, array.Count);
foreach (JToken item in array)
{
ElementSchema.AddValue(item);
}
}
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
ElementSchema.Beautify();
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return $"array<{ElementSchema.GetTypeName()}>";
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return new()
{
["min_count"] = MinCount.ToString(),
["max_count"] = MaxCount.ToString(),
["value_type"] = ElementSchema.ToString(),
};
}
}
@@ -0,0 +1,77 @@
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Pf2e.Scrapper.SchemaBuilder.Types;
/// <summary>
/// Тип представоления словаря в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal class DictionarySchemaType(SchemaNode parent) : AbstractSchemaType(parent)
{
/// <summary>
/// Схема ключей словаря.
/// </summary>
public readonly SchemaNode KeyNode = new(parent, "[key]");
/// <summary>
/// Схема значений словаря.
/// </summary>
public readonly SchemaNode ItemNode = new(parent, "[value]");
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{
return Resolver.TokenType(value) == JTokenType.Object;
}
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
{
JObject obj = (JObject)value;
foreach (KeyValuePair<string, JToken?> pair in obj)
{
KeyNode.AddValue(pair.Key);
ItemNode.AddValue(pair.Value);
}
}
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
KeyNode.Beautify();
ItemNode.Beautify();
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return $"dict<{KeyNode.GetTypeName()}, {ItemNode.GetTypeName()}>";
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return new Dictionary<string, string>()
{
["_key"] = KeyNode.ToString(),
["_value"] = ItemNode.ToString(),
};
}
}
@@ -0,0 +1,50 @@
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace Pf2e.Scrapper.SchemaBuilder.Types;
/// <summary>
/// Тип, указывающий, что данные игнорируются.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal class IgnoreSchemaType(SchemaNode parent) : AbstractSchemaType(parent)
{
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{
return true;
}
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public override void AddValue(JToken value) { }
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify() { }
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return "ignore";
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return [];
}
}
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace Pf2e.Scrapper.SchemaBuilder.Types;
/// <summary>
/// Тип представоления объектов в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal class ObjectSchemaType(SchemaNode parent) : AbstractSchemaType(parent)
{
/// <summary>
/// Элемнты полей объекта.
/// </summary>
public readonly List<SchemaNode> Fields = [];
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{
return Resolver.TokenType(value) == JTokenType.Object;
}
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
{
JObject obj = (JObject)value;
foreach (SchemaNode field in Fields.Where(field => !obj.ContainsKey(field.Name)))
{
field.IsNullable = true;
}
foreach (KeyValuePair<string, JToken?> pair in obj)
{
SchemaNode? field = Fields.FirstOrDefault(f => f.Name == pair.Key);
if (field is null)
{
field = new(Parent, pair.Key);
Fields.Add(field);
}
field.AddValue(pair.Value);
}
}
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
Fields.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
foreach (SchemaNode field in Fields)
{
field.Beautify();
}
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return "object";
}
/// <summary>
/// Получение данных для форматирования структуры в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public Dictionary<string, string> GetSchemaYaml()
{
Dictionary<string, string> result = new();
foreach (SchemaNode field in Fields)
{
string s = field.Name;
s = field.IsNullable ? $"[{s}]" : $"'{s}'";
result.Add(s, field.ToString());
}
return result;
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return new() { ["schema"] = SchemaNode.FormatYaml(GetSchemaYaml()) };
}
}
@@ -0,0 +1,109 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace Pf2e.Scrapper.SchemaBuilder.Types;
/// <summary>
/// Тип представоления простых значений в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
/// <param name="type">Тип значения.</param>
internal class StaticSchemaType(SchemaNode parent, JTokenType type) : AbstractSchemaType(parent)
{
/// <summary>
/// Тип значения.
/// </summary>
protected readonly JTokenType Type = type;
/// <summary>
/// Набор возможных значений и их количества.
/// </summary>
protected Dictionary<JToken, int> Values = [];
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{
return value.Type == Type;
}
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
{
Values.TryAdd(value, 0);
Values[value]++;
}
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
if (Type == JTokenType.String)
{
Dictionary<JToken, int> newValues = new();
foreach (KeyValuePair<JToken, int> pair in Values)
{
string newValue = "\"" + pair.Key.ToString().Trim().Replace("\"", "\\\"") + "\"";
newValues.TryAdd(newValue, 0);
newValues[newValue] += pair.Value;
}
Values = newValues;
}
Values = Values
.OrderByDescending(pair => pair.Value)
.ToDictionary(pair => pair.Key, pair => pair.Value);
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return Type.ToString().ToLower();
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
List<string> values = [];
int count = Values.Count;
string prefix = (count == 1) ? "" : "- ";
if (
count is <= SchemaNode.ElementLimit and > 0
&& (Type != JTokenType.String || !Values.Any(s => s.ToString().Contains('\n')))
)
{
foreach (KeyValuePair<JToken, int> kvp in Values)
{
values.Add($"{prefix}{kvp.Key} (x{kvp.Value})");
}
}
return new()
{
["different_values"] =
count.ToString()
+ ((count == Root.Values) ? " (all different)" : "")
+ ((count == 1) ? " (all same)" : ""),
["variants"] = (values.Count > 0) ? string.Join("\r\n", values) : "[omitted]",
};
}
}