diff --git a/CSharpBible/Data/Data.sln b/CSharpBible/Data/Data.sln
index a5e22fdd2..2fd30a711 100644
--- a/CSharpBible/Data/Data.sln
+++ b/CSharpBible/Data/Data.sln
@@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11111.16
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoMigrator.Core", "RepoMigrator\RepoMigrator.Core\RepoMigrator.Core\RepoMigrator.Core.csproj", "{5281BA89-6796-4C3E-8DDA-7A627896AC1A}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RepoMigrator", "RepoMigrator", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
Directory.Build.props = Directory.Build.props
@@ -72,6 +70,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PictureDB.UI", "PictureDB\P
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommonDialogs_net", "..\Libraries\CommonDialogs\CommonDialogs_net.csproj", "{65F08D9B-F63E-14C2-C35D-C324E1E37785}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoMigrator.Core", "RepoMigrator\RepoMigrator.Core\RepoMigrator.Core.csproj", "{7C507273-B03C-6545-08E2-F89BFF1DEDAA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -82,18 +82,6 @@ Global
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|x64.Build.0 = Debug|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Debug|x86.Build.0 = Debug|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|Any CPU.Build.0 = Release|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|x64.ActiveCfg = Release|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|x64.Build.0 = Release|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|x86.ActiveCfg = Release|Any CPU
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A}.Release|x86.Build.0 = Release|Any CPU
{387BDBC9-0123-4C86-98FA-FBF66522A4B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{387BDBC9-0123-4C86-98FA-FBF66522A4B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{387BDBC9-0123-4C86-98FA-FBF66522A4B9}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -382,12 +370,23 @@ Global
{65F08D9B-F63E-14C2-C35D-C324E1E37785}.Release|x64.Build.0 = Release|Any CPU
{65F08D9B-F63E-14C2-C35D-C324E1E37785}.Release|x86.ActiveCfg = Release|x86
{65F08D9B-F63E-14C2-C35D-C324E1E37785}.Release|x86.Build.0 = Release|x86
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|x64.Build.0 = Debug|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Debug|x86.Build.0 = Debug|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|x64.ActiveCfg = Release|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|x64.Build.0 = Release|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|x86.ActiveCfg = Release|Any CPU
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {5281BA89-6796-4C3E-8DDA-7A627896AC1A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{387BDBC9-0123-4C86-98FA-FBF66522A4B9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{0256D739-F882-4F8C-8820-570FB07687AA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{DB10ACCB-609B-4638-8629-89196580CB43} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
@@ -410,6 +409,7 @@ Global
{42156DBB-5FC0-3FE1-FC43-55400E7FDFAD} = {3C73C616-12F2-478C-9CAC-823780861BCD}
{BFCB4F4A-F6A3-EB13-DB02-B0C1979AFDEA} = {3C73C616-12F2-478C-9CAC-823780861BCD}
{65F08D9B-F63E-14C2-C35D-C324E1E37785} = {51F6C20B-003C-430C-BED7-2A89F834E4C0}
+ {7C507273-B03C-6545-08E2-F89BFF1DEDAA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {128BE64A-28F5-47C5-A045-2352EF09BFBB}
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj
new file mode 100644
index 000000000..ddd4f0052
--- /dev/null
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/DataAnalysis.Core.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+ net9.0;net8.0
+ false
+ 12.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/Export/TableExcelExporterTests.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/Export/TableExcelExporterTests.cs
new file mode 100644
index 000000000..42496f870
--- /dev/null
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core.Tests/Export/TableExcelExporterTests.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Reflection;
+using ClosedXML.Excel;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NSubstitute;
+using DataAnalysis.Core.Export;
+
+namespace DataAnalysis.Core.Tests;
+
+// Pseudocode (Plan):
+// 1. Per Reflection die private Methode ToXLCellValue ermitteln: BindingFlags.Instance | BindingFlags.NonPublic.
+// 2. Instanz von TableExcelExporter erzeugen.
+// 3. DataTestMethod mit mehreren DataRow Fällen:
+// - null -> string.Empty
+// - DBNull.Value -> string.Empty
+// - bool true/false -> 1/0 (int)
+// - int -> int
+// - long im int-Bereich -> int
+// - long außerhalb int-Bereich -> double
+// - float/double Ganzzahl-Wert -> int
+// - float/double mit Nachkommastellen -> double
+// - double.NaN / double.PositiveInfinity -> kulturinvariant string ("NaN","Infinity")
+// - string leer/Whitespace -> string.Empty
+// - string Integer -> int
+// - string Double -> double
+// 4. Methode via MethodInfo.Invoke aufrufen, Ergebnis (XLCellValue) typ und Wert prüfen.
+// 5. Separater Test: Interface ITableExporter via NSubstitute erzeugen und verifizieren,
+// dass Reflection auf Interface die Methode nicht findet, aber auf konkretem Typ schon.
+// 6. Assertions kurz und präzise.
+// 7. Nutzung NSubstitute sicherstellen (Substitute.For()).
+
+[TestClass]
+public class TableExcelExporterTests
+{
+ private static MethodInfo GetPrivateMethod(string name)
+ => typeof(TableExcelExporter).GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException($"Methode {name} nicht gefunden.");
+
+ [DataTestMethod]
+ [DataRow(TypeCode.Empty, null, "", TypeCode.String)] // null -> ""
+ [DataRow(TypeCode.DBNull, null, "", TypeCode.String)] // DBNull -> ""
+ [DataRow(TypeCode.Boolean, true, true, TypeCode.Boolean)] // true -> 1
+ [DataRow(TypeCode.Boolean, false, false, TypeCode.Boolean)] // false -> 0
+ [DataRow(TypeCode.Int32, 5, 5, TypeCode.Int32)] // int bleibt int
+ [DataRow(TypeCode.Int64, 5L, 5, TypeCode.Int32)] // long im int-Bereich -> int
+ [DataRow(TypeCode.Int64, 5000000000L, 5000000000d, TypeCode.Double)] // long außerhalb int-Bereich -> double
+ [DataRow(TypeCode.Single, 5.0f, 5, TypeCode.Int32)] // float Ganzzahl -> int
+ [DataRow(TypeCode.Single, 5.25f, 5.25d, TypeCode.Double)] // float mit Nachkommastellen -> double
+ [DataRow(TypeCode.Double, 5.0d, 5, TypeCode.Int32)] // double Ganzzahl -> int
+ [DataRow(TypeCode.Double, 5.125d, 5.125d, TypeCode.Double)] // double mit Nachkommastellen -> double
+ [DataRow(TypeCode.Double, double.NaN, "NaN", TypeCode.String)] // NaN -> string "NaN"
+ [DataRow(TypeCode.Double, double.PositiveInfinity, "Infinity", TypeCode.String)] // Infinity -> string
+ [DataRow(TypeCode.String, " ", "", TypeCode.String)] // Whitespace -> ""
+ [DataRow(TypeCode.String, "True", true, TypeCode.Boolean)] // String int -> int
+ [DataRow(TypeCode.String, "false", false, TypeCode.Boolean)] // String int -> int
+ [DataRow(TypeCode.String, "42", 42, TypeCode.Int32)] // String int -> int
+ [DataRow(TypeCode.String, "42.75", 42.75d, TypeCode.Double)] // String double (.) -> double
+ [DataRow(TypeCode.String, "1e-6", 1e-6d, TypeCode.Double)] // String double (.) -> double
+ [DataRow(TypeCode.String, "-4.2e3", -4.2e3d, TypeCode.Double)] // String double (.) -> double
+ [DataRow(TypeCode.String, "42,75", 42.75d, TypeCode.Double)] // String double (,) -> double
+ [DataRow(TypeCode.String, "42.7.5", "42.7.5", TypeCode.String)] // Ungültig -> string unverändert
+ [DataRow(TypeCode.String, "0010", "0010", TypeCode.String)] // Führende Nullen -> string
+ [DataRow(TypeCode.String, "0", 0, TypeCode.Int32)] // nur Null -> int
+ public void ToXLCellValue_Test(TypeCode inputTypeCode, object? inputRaw, object expectedValue, TypeCode expectedTypeCode)
+ {
+ // Eingabeobjekt aus TypeCode ableiten
+ object? input = inputTypeCode switch
+ {
+ TypeCode.Empty => null,
+ TypeCode.DBNull => DBNull.Value,
+ _ => inputRaw
+ };
+
+ // Erwarteten Typ aus TypeCode bestimmen
+ Type expectedType = expectedTypeCode switch
+ {
+ TypeCode.String => typeof(string),
+ TypeCode.Int32 => typeof(int),
+ TypeCode.Boolean => typeof(bool),
+ TypeCode.Double => typeof(double),
+ _ => throw new AssertFailedException("Nicht unterstützter erwarteter TypeCode.")
+ };
+
+ var exporter = new TableExcelExporter();
+ var mi = GetPrivateMethod("ToXLCellValue");
+
+ var xl = (XLCellValue)mi.Invoke(exporter, new[] { input })!;
+
+ object actualValue;
+ TypeCode actualTC;
+ if (xl.IsBlank)
+ {
+ actualValue = "";
+ actualTC = TypeCode.String;
+ }
+ else if (xl.IsText)
+ {
+ actualValue = xl.GetText();
+ actualTC = TypeCode.String;
+ }
+ else if (xl.IsNumber)
+ {
+ actualValue = xl.GetNumber();
+ actualTC = Math.Abs((double)actualValue % 1d) < 1e-10 ? TypeCode.Int32 : TypeCode.Double;
+ }
+ else if (xl.IsBoolean)
+ {
+ actualValue = xl.GetBoolean();
+ actualTC = TypeCode.Boolean;
+ }
+ else
+ {
+ actualValue = xl.ToString();
+ actualTC = TypeCode.String;
+ }
+
+ if (expectedType == typeof(string))
+ {
+ Assert.AreEqual(expectedValue.ToString(), actualValue?.ToString(), "Stringwert stimmt nicht.");
+ return;
+ }
+
+ if (expectedType == typeof(int))
+ {
+ Assert.AreEqual(expectedTypeCode, actualTC , "Erwartet int.");
+ Assert.AreEqual((int)expectedValue, (int)Math.Round((double)actualValue), "int-Wert stimmt nicht.");
+ }
+ else if (expectedType == typeof(double))
+ {
+ Assert.IsTrue(actualValue is double or int, "Erwartet double oder int Repräsentation.");
+ double actualDouble = actualValue is int ai ? ai : (double)actualValue;
+ Assert.AreEqual(Convert.ToDouble(expectedValue), actualDouble, 1e-12, "double-Wert stimmt nicht.");
+ }
+ else if (expectedType == typeof(bool))
+ {
+ Assert.IsInstanceOfType(actualValue, typeof(bool), "Erwartet bool.");
+ Assert.AreEqual((bool)expectedValue, (bool)actualValue, "bool-Wert stimmt nicht.");
+ }
+ else
+ {
+ Assert.Fail("Nicht unterstützter erwarteter Typ.");
+ }
+ }
+
+ [TestMethod]
+ public void Reflection_Finden_Der_Privaten_Methode_Ueber_Konkreten_Typ_Nicht_Ueber_Interface()
+ {
+ // Arrange
+ var ifaceSub = Substitute.For();
+ var viaInterface = ifaceSub.GetType().GetMethod("ToXLCellValue", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ var viaConcrete = typeof(TableExcelExporter).GetMethod("ToXLCellValue", BindingFlags.Instance | BindingFlags.NonPublic);
+
+ // Assert
+ Assert.IsNull(viaInterface, "Private Methode sollte über Interface-Proxy nicht gefunden werden.");
+ Assert.IsNotNull(viaConcrete, "Private Methode sollte über konkreten Typ gefunden werden.");
+ }
+}
\ No newline at end of file
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/Interfaces/ITableExporter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/Interfaces/ITableExporter.cs
index 12f30de78..b8039cac0 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/Interfaces/ITableExporter.cs
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/Interfaces/ITableExporter.cs
@@ -6,5 +6,5 @@ namespace DataAnalysis.Core.Export.Interfaces;
public interface ITableExporter
{
- Task ExportAsync(DataTable table, string inputPath, string? outputPath, CancellationToken cancellationToken = default);
+ Task ExportAsync(DataTable table, string inputPath, string? outputPath, CancellationToken cancellationToken = default, Action? progressCallback = null);
}
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/TableExcelExporter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/TableExcelExporter.cs
index 54f405517..43585f0b3 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/TableExcelExporter.cs
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Export/TableExcelExporter.cs
@@ -6,12 +6,13 @@
using System.Threading.Tasks;
using ClosedXML.Excel;
using DataAnalysis.Core.Export.Interfaces;
+using DocumentFormat.OpenXml.Drawing.Diagrams;
namespace DataAnalysis.Core.Export;
public sealed class TableExcelExporter : ITableExporter
{
- public Task ExportAsync(DataTable table, string inputPath, string? outputPath, CancellationToken cancellationToken = default)
+ public Task ExportAsync(DataTable table, string inputPath, string? outputPath, CancellationToken cancellationToken = default, Action? progressCallback = null)
{
outputPath ??= BuildDefaultOutputPath(inputPath);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
@@ -19,32 +20,147 @@ public Task ExportAsync(DataTable table, string inputPath, string? outpu
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet(string.IsNullOrWhiteSpace(table.TableName) ? "Tabelle" : TrimSheetName(table.TableName));
+ int totalRows = table.Rows.Count;
+ int processedRows = 0;
+ DateTime lastReport = DateTime.UtcNow;
+ void Report()
+ {
+ if (progressCallback is null)
+ return;
+ if ((DateTime.UtcNow - lastReport).TotalSeconds >= 1)
+ lock (progressCallback)
+ {
+ progressCallback(Math.Clamp(totalRows == 0 ? 1.0 : (double)processedRows / totalRows*0.7d, 0d, 0.7d));
+ lastReport = DateTime.UtcNow;
+ }
+ }
+
// Header
for (int c = 0; c < table.Columns.Count; c++)
ws.Cell(1, c + 1).Value = table.Columns[c].ColumnName;
ws.Range(1, 1, 1, Math.Max(1, table.Columns.Count)).Style.Font.Bold = true;
// Rows
- int r = 2;
- foreach (DataRow row in table.Rows)
+
+ Parallel.For(2, table.Rows.Count + 2, (r) =>
{
+ var row = table.Rows[r - 2];
cancellationToken.ThrowIfCancellationRequested();
for (int c = 0; c < table.Columns.Count; c++)
if (table.Columns[c].DataType == typeof(DateTime) && row[c] is DateTime dt)
{
- ws.Cell(r, c + 1).Value = dt;
- ws.Cell(r, c + 1).Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss.ms";
+ lock (ws)
+ {
+ ws.Cell(r, c + 1).Value = dt;
+ ws.Cell(r, c + 1).Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss.ms";
+ }
}
else
- ws.Cell(r, c + 1).Value = row[c]?.ToString() ?? string.Empty;
- r++;
- }
+ {
+ var value = ToXLCellValue(row[c]);
+ // if (r==2)
+ // value =
+ lock (ws)
+ ws.Cell(r, c + 1).Value = value;
+ }
+ processedRows++;
+ if (r % 100 == 0)
+ Report();
+ });
- ws.Columns().AdjustToContents();
+ // ws.Columns().AdjustToContents();
+ progressCallback?.Invoke(0.8); // Abschluss
wb.SaveAs(outputPath);
+ progressCallback?.Invoke(1.0); // Abschluss
return Task.FromResult(outputPath);
}
+ private void UnParallel_For(int v1, int v2, Action value)
+ {
+ for (var i = v1; i < v2; i++)
+ value(i);
+ }
+
+ private XLCellValue ToXLCellValue(object v)
+ {
+ static bool IsWholeNumber(double d) => Math.Abs(d % 1) < 1e-10;
+
+ if (v is null || v == DBNull.Value)
+ return string.Empty;
+
+ if (v is bool b)
+ return b ? true : false;
+
+ switch (v)
+ {
+ case int i:
+ return i;
+ case long l:
+ if (l >= int.MinValue && l <= int.MaxValue)
+ return (int)l;
+ return (double)l;
+ case short s:
+ return (int)s;
+ case byte by:
+ return (int)by;
+ case sbyte sb:
+ return (int)sb;
+ case uint ui:
+ if (ui <= int.MaxValue)
+ return (int)ui;
+ return (double)ui;
+ case ulong ul:
+ if (ul <= (ulong)int.MaxValue)
+ return (int)ul;
+ return (double)ul;
+ case float f:
+ if (float.IsNaN(f) || float.IsInfinity(f))
+ return f.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ if (IsWholeNumber(f) && f >= int.MinValue && f <= int.MaxValue)
+ return (int)f;
+ return (double)f;
+ case double d:
+ if (double.IsNaN(d) || double.IsInfinity(d))
+ return d.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ if (IsWholeNumber(d) && d >= int.MinValue && d <= int.MaxValue)
+ return (int)d;
+ return d;
+ case string str:
+ {
+ if (str.ToLower() == "true")
+ return true;
+ if (str.ToLower() == "false")
+ return false;
+
+ str = str.Trim();
+ if (str.Length == 0)
+ return string.Empty;
+
+ if (int.TryParse(str, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var si))
+ {
+ if (!str.StartsWith('0') || str == "0")
+ return si;
+ else
+ return str;
+ }
+
+ if (double.TryParse(str, System.Globalization.NumberStyles.Float | System.Globalization.NumberStyles.AllowThousands,
+ System.Globalization.CultureInfo.InvariantCulture, out var sd))
+ {
+ if (str.StartsWith('0'))
+ return str;
+ if (IsWholeNumber(sd) && sd >= int.MinValue && sd <= int.MaxValue)
+ return (int)sd;
+ return sd;
+ }
+
+ return str;
+ }
+ }
+
+ return v.ToString() ?? string.Empty;
+ }
+
private static string BuildDefaultOutputPath(string inputPath)
{
var dir = Path.GetDirectoryName(inputPath)!;
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/DelimitedTableReader.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/DelimitedTableReader.cs
index fa3b3d76c..8dc36836a 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/DelimitedTableReader.cs
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/DelimitedTableReader.cs
@@ -8,12 +8,12 @@
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
+using System.Threading.Channels;
namespace DataAnalysis.Core.Import;
@@ -30,7 +30,7 @@ public DelimitedTableReader(IDelimitedTableParsingProfile profile)
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
}
- public async Task ReadTableAsync(string inputPath, CancellationToken cancellationToken = default)
+ public async Task ReadTableAsync(string inputPath, CancellationToken cancellationToken = default, Action? progressCallback = null)
{
if (string.IsNullOrWhiteSpace(inputPath))
throw new ArgumentException(ErrorPathEmpty, nameof(inputPath));
@@ -39,6 +39,8 @@ public async Task ReadTableAsync(string inputPath, CancellationToken
var dt = new DataTable(_profile.TableName);
using var fs = new FileStream(inputPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ long totalBytes = fs.Length;
+ long processedBytes = 0;
using var reader = new StreamReader(fs, DetectEncoding(fs) ?? Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
// Header
@@ -50,10 +52,11 @@ public async Task ReadTableAsync(string inputPath, CancellationToken
var headerLine = await reader.ReadLineAsync().ConfigureAwait(false);
if (headerLine is not null)
{
+ processedBytes += Encoding.UTF8.GetByteCount(headerLine) + Environment.NewLine.Length;
var headers = SplitLine(headerLine, _profile.Delimiter, _profile.Quote, _profile.TrimWhitespace);
headerMap = headers.Select((h, i) => new { Name = (h ?? string.Empty).Trim(), i })
- .GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
- .ToDictionary(g => g.Key, g => g.First().i, StringComparer.OrdinalIgnoreCase);
+ .GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(g => g.Key, g => g.First().i, StringComparer.OrdinalIgnoreCase);
inclFields.AddRange(headerMap.Select((h, i) => i));
foreach (var h in headerMap)
{
@@ -62,7 +65,6 @@ public async Task ReadTableAsync(string inputPath, CancellationToken
if (_profile.ExtractionRules.Any((r) => r.SourceColumn == h.Key))
inclFields.Remove(h.Value);
}
-
}
}
@@ -71,50 +73,144 @@ public async Task ReadTableAsync(string inputPath, CancellationToken
{
EnsureColumn(dt, mapping.Target, mapping.IsDateTime);
}
- foreach (var i in inclFields)
+ if (headerMap is not null)
+ {
+ foreach (var i in inclFields)
+ {
+ var hmi = headerMap.Values.FirstOrDefault(v => v == i);
+ EnsureColumn(dt, headerMap.Keys.ToArray()[hmi]);
+ }
+ }
+
+ DateTime lastReport = DateTime.UtcNow;
+ void ReportProgress()
+ {
+ if (progressCallback is null || totalBytes == 0)
+ return;
+ if ((DateTime.UtcNow - lastReport).TotalSeconds >= 1)
+ {
+ progressCallback(Math.Clamp((double)processedBytes / totalBytes, 0d, 1d));
+ lastReport = DateTime.UtcNow;
+ }
+ }
+ ;
+
+ var headerKeys = headerMap?.Keys.ToArray();
+
+
+ /*
+ // PARALLELE VERARBEITUNG (Producer / Consumer)
+ var channel = Channel.CreateBounded(new BoundedChannelOptions(2048)
+ {
+ SingleWriter = true,
+ SingleReader = false,
+ FullMode = BoundedChannelFullMode.Wait
+ });
+
+ int workerCount = Math.Max(1, Environment.ProcessorCount);
+
+ var consumers = Enumerable.Range(0, workerCount).Select(_ => Task.Run(async () =>
{
- var hmi = headerMap.Values.FirstOrDefault((v) => (v == i));
- EnsureColumn(dt, headerMap.Keys.ToArray()[hmi]);
+ await foreach (var line in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
+ {
+ var flowControl = ProcessLine(dt, headerMap, inclFields, headerKeys, line);
+ if (!flowControl)
+ {
+ continue;
+ }
+
+ }
+ }, cancellationToken)).ToArray();
+
+ // Producer liest Datei
+ string? readLine;
+ while ((readLine = await reader.ReadLineAsync().ConfigureAwait(false)) is not null)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ processedBytes += Encoding.UTF8.GetByteCount(readLine) + Environment.NewLine.Length;
+ ReportProgress();
+ await channel.Writer.WriteAsync(readLine, cancellationToken).ConfigureAwait(false);
}
+ channel.Writer.Complete();
- string? line;
- while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) is not null)
+ await Task.WhenAll(consumers).ConfigureAwait(false);
+ */
+ while (!reader.EndOfStream)
{
+ var line = await reader.ReadLineAsync().ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
- if (string.IsNullOrWhiteSpace(line))
+ processedBytes += Encoding.UTF8.GetByteCount(line) + Environment.NewLine.Length;
+ ReportProgress();
+ var flowControl = ProcessLine(dt, headerMap, inclFields, headerKeys, line);
+ if (!flowControl)
+ {
continue;
- var fields = SplitLine(line, _profile.Delimiter, _profile.Quote, _profile.TrimWhitespace);
+ }
+ }
+
+ progressCallback?.Invoke(1.0);
+ return dt;
+ }
+
+ private bool ProcessLine(DataTable dt, Dictionary? headerMap, List inclFields, string[]? headerKeys, string? line)
+ {
+ if (string.IsNullOrWhiteSpace(line))
+ return false;
+
+ var fields = SplitLine(line, _profile.Delimiter, _profile.Quote, _profile.TrimWhitespace);
- // extraction rules
- var attributes = new Dictionary(StringComparer.OrdinalIgnoreCase);
- ApplyExtractionRules(fields, headerMap, attributes);
+ // extraction rules
+ var attributes = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ ApplyExtractionRules(fields, headerMap, attributes);
- // grow columns for attributes if needed
+ // grow columns for attributes if needed
+ if (attributes.Count > 0)
+ {
foreach (var key in attributes.Keys)
{
if (!_profile.FixedColumns.Any((f) => f.Source == key))
EnsureColumn(dt, key);
}
+ }
- var row = dt.NewRow();
+ // DataRow füllen
+ var rowValues = new Dictionary(StringComparer.OrdinalIgnoreCase);
- foreach (var mapping in _profile.FixedColumns)
- row[mapping.Target] = Extract(mapping, fields, headerMap, attributes);
+ foreach (var mapping in _profile.FixedColumns)
+ {
+ var value = Extract(mapping, fields, headerMap, attributes);
+ rowValues[mapping.Target] = value ?? string.Empty;
+ }
- foreach (var i in inclFields)
+ if (headerMap is not null && headerKeys is not null)
+ {
+ foreach (var iField in inclFields)
{
- var hmi = headerMap.Values.FirstOrDefault((v) => (v == i));
- if (fields.Count>i)
- row[headerMap.Keys.ToArray()[hmi]] = fields[i];
+ if (fields.Count > iField)
+ {
+ var hmi = headerMap.Values.FirstOrDefault(v => v == iField);
+ var colName = headerKeys[hmi];
+ rowValues[colName] = fields[iField] ?? string.Empty;
+ }
}
+ }
- foreach (var kv in attributes)
- if (!_profile.FixedColumns.Any((f) => f.Source == kv.Key))
- row[kv.Key] = kv.Value ?? string.Empty;
- dt.Rows.Add(row);
+ foreach (var kv in attributes)
+ {
+ if (!_profile.FixedColumns.Any((f) => f.Source == kv.Key))
+ rowValues[kv.Key] = kv.Value ?? string.Empty;
}
- return dt;
+
+ lock (dt)
+ {
+ var row = dt.NewRow();
+ foreach (var kv in rowValues)
+ row[kv.Key] = kv.Value ?? string.Empty;
+
+ dt.Rows.Add(row);
+ }
+ return true;
}
private void ApplyExtractionRules(IReadOnlyList fields, Dictionary? headerMap, Dictionary attributes)
@@ -152,7 +248,6 @@ private void ApplyExtractionRules(IReadOnlyList fields, Dictionary fields, Dictionary fields,
- Dictionary? headerMap,
- IReadOnlyDictionary? attributes = null)
+ FixedColumnMapping mapping,
+ IReadOnlyList fields,
+ Dictionary? headerMap,
+ IReadOnlyDictionary? attributes = null)
{
- // Hilfsfunktionen
int? ResolveFieldIndex(string? selector)
{
if (string.IsNullOrWhiteSpace(selector))
@@ -199,9 +293,7 @@ private string Extract(
return null;
}
- // Werte über fields (Index/Header) ODER attributes extrahieren
- var tsRaw = GetBySelector(mapping.Source, out var _);
-
+ var tsRaw = GetBySelector(mapping.Source, out _);
return tsRaw;
}
@@ -233,9 +325,14 @@ private string Extract(
if (quote.HasValue && c == q)
{
if (inQuotes && i + 1 < line.Length && line[i + 1] == q)
- { sb.Append(q); i++; }
+ {
+ sb.Append(q);
+ i++;
+ }
else
- { inQuotes = !inQuotes; }
+ {
+ inQuotes = !inQuotes;
+ }
continue;
}
if (!inQuotes && c == delimiter)
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/Interfaces/ITableReader.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/Interfaces/ITableReader.cs
index 19ee41d88..9fdc29cb5 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/Interfaces/ITableReader.cs
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Import/Interfaces/ITableReader.cs
@@ -4,5 +4,5 @@ namespace DataAnalysis.Core.Import.Interfaces;
public interface ITableReader
{
- Task ReadTableAsync(string inputPath, CancellationToken cancellationToken = default);
+ Task ReadTableAsync(string inputPath, CancellationToken cancellationToken = default, Action? progressCallback = null);
}
\ No newline at end of file
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/AnalysisAggregateProfile.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/AnalysisAggregateProfile.cs
index 62d21f842..31ff305ec 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/AnalysisAggregateProfile.cs
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/AnalysisAggregateProfile.cs
@@ -24,7 +24,29 @@ public sealed class AnalysisAggregateProfile
new AnalysisQuery { Title = "Top-Events2", Dimensions = new [] { DimensionKind.MessageNormalized }, TopN =30, Filter = new ValueFilterDefinition { Field = "Severity", Type = "Enum", Operator = FilterOperator.Le, Value = "Error" }, },
new AnalysisQuery { Title = "Severity x Source", Dimensions = new [] { DimensionKind.Source, DimensionKind.Severity }, Columns = Enum.GetValues().Select(s => s.ToString()).ToArray() },
new AnalysisQuery { Title = "Events x Source (Top50)", Dimensions = new [] { DimensionKind.Source, DimensionKind.MessageNormalized }, TopN =300},
- new AnalysisQuery { Title = "Koordinate Cluster", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 30, IsDBScan = true, DbEps = 10.0, DbMinPts = 3, Filter = new ValueFilterDefinition { Field = "Severity", Type = "Enum", Operator = FilterOperator.Le, Value = "Error" }, }
+ new AnalysisQuery { Title = "Error Cluster", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 30, IsDBScan = true, DbEps = 2.0, DbMinPts = 3, Filter = new ValueFilterDefinition { Field = "Severity", Type = "Enum", Operator = FilterOperator.Le, Value = "Error" }, },
+ new AnalysisQuery { Title = "Max-G Cluster (1.0)", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 30, IsDBScan = true, DbEps = 1.0, DbMinPts = 3, Filter = new GroupFilterDefinition { Mode="And", Type = "group",
+ Filters=[ new ValueFilterDefinition { Field = "Message", Type = "String", Operator = FilterOperator.Eq, Value = "Max. G-Force" },
+ new ValueFilterDefinition { Field = "X", Type = "value", Operator = FilterOperator.Gt, Value = "10" }
+ ] }, },
+ new AnalysisQuery { Title = "Max-G Cluster (0.5)", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 50, IsDBScan = true, DbEps = 0.5, DbMinPts = 3, Filter =
+ new GroupFilterDefinition { Mode="And", Type = "group",
+ Filters=[
+ new ValueFilterDefinition { Field = "Message", Type = "String", Operator = FilterOperator.Eq, Value = "Max. G-Force" },
+ new ValueFilterDefinition { Field = "X", Type = "value", Operator = FilterOperator.Gt, Value = "10" }
+ ] }, },
+ new AnalysisQuery { Title = "SSCU Cluster (0.5)", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 30, IsDBScan = true, DbEps = 0.5, DbMinPts = 3, Filter =
+ new GroupFilterDefinition { Mode="And", Type = "group", Filters=[
+ new ValueFilterDefinition { Field = "Message", Type = "String", Operator = FilterOperator.StartsWith, Value = "SSCU" },
+ new ValueFilterDefinition { Field = "X", Type = "value", Operator = FilterOperator.Gt, Value = "10" },
+ ]
+ }, },
+ new AnalysisQuery { Title = "SSCU Cluster (0.25)", Dimensions = new [] { DimensionKind.X, DimensionKind.Y }, TopN = 50, IsDBScan = true, DbEps = 0.25, DbMinPts = 3, Filter =
+ new GroupFilterDefinition { Mode="And", Type = "group", Filters=[
+ new ValueFilterDefinition { Field = "Message", Type = "String", Operator = FilterOperator.StartsWith, Value = "SSCU" },
+ new ValueFilterDefinition { Field = "X", Type = "value", Operator = FilterOperator.Gt, Value = "10" },
+ ] },
+ }
}
};
}
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/Filters/FilterDefinitions.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/Filters/FilterDefinitions.cs
index 4e0577214..8379558dd 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/Filters/FilterDefinitions.cs
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.Core/Models/Filters/FilterDefinitions.cs
@@ -9,7 +9,7 @@ namespace DataAnalysis.Core.Models;
///
public abstract class FilterDefinition
{
- [JsonPropertyName("type")] public required string Type { get; init; }
+ [JsonPropertyName("type")] public string Type { get; init; }
}
public sealed class ValueFilterDefinition : FilterDefinition
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF.TestHarness/MainWindow.xaml.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF.TestHarness/MainWindow.xaml.cs
index 4edbbfb24..493e11e41 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF.TestHarness/MainWindow.xaml.cs
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF.TestHarness/MainWindow.xaml.cs
@@ -56,8 +56,8 @@ private void OnLoadClusters(object sender, RoutedEventArgs e)
{
[new Vector2(0, 0)] = 15000,
[new Vector2(100000, 50000)] = 2000,
- [new Vector2(-50000, 80000)] = 8000,
- [new Vector2(800000, -230000)] = 300
+ [new Vector2(-50000, 80000)] = 800,
+ [new Vector2(800000, -230000)] = 300000
};
var agg = new AggregationResult
{
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/ViewModels/ClusterAggregationViewModel.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/ViewModels/ClusterAggregationViewModel.cs
index ed5284a8b..312ede925 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/ViewModels/ClusterAggregationViewModel.cs
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/ViewModels/ClusterAggregationViewModel.cs
@@ -8,40 +8,29 @@ namespace DataAnalysis.WPF.ViewModels;
public sealed partial class ClusterAggregationViewModel : AggregationItemViewModel
{
- public sealed class PointItem
- {
- public double X { get; init; }
- public double Y { get; init; }
- public int Count { get; init; }
- }
+ public sealed class PointItem { public double X { get; init; } public double Y { get; init; } public int Count { get; init; } }
+ public sealed class AxisTick { public double Value { get; init; } public string Label { get; init; } = string.Empty; }
public ObservableCollection Points { get; } = new();
public ObservableCollection RawPoints { get; } = new();
+ public ObservableCollection XTicks { get; } = new();
+ public ObservableCollection YTicks { get; } = new();
- [ObservableProperty]
- private double minX;
- [ObservableProperty]
- private double maxX;
- [ObservableProperty]
- private double minY;
- [ObservableProperty]
- private double maxY;
- [ObservableProperty]
- private int maxCount;
+ [ObservableProperty] private double minX;
+ [ObservableProperty] private double maxX;
+ [ObservableProperty] private double minY;
+ [ObservableProperty] private double maxY;
+ [ObservableProperty] private int maxCount;
+ [ObservableProperty] private bool hasZeroX;
+ [ObservableProperty] private bool hasZeroY;
- public ClusterAggregationViewModel(AggregationResult agg)
- : base(agg.Title)
- {
- Rebuild(agg);
- }
+ public ClusterAggregationViewModel(AggregationResult agg) : base(agg.Title) { Rebuild(agg); }
private void Rebuild(AggregationResult agg)
{
- Points.Clear();
- RawPoints.Clear();
+ Points.Clear(); RawPoints.Clear(); XTicks.Clear(); YTicks.Clear();
double minx = double.PositiveInfinity, maxx = double.NegativeInfinity;
- double miny = double.PositiveInfinity, maxy = double.NegativeInfinity;
- int maxc = 0;
+ double miny = double.PositiveInfinity, maxy = double.NegativeInfinity; int maxc = 0;
if (agg.Series is not null)
{
foreach (var kv in agg.Series)
@@ -49,116 +38,77 @@ private void Rebuild(AggregationResult agg)
if (TryGetXY(kv.Key, out var x, out var y))
{
Points.Add(new PointItem { X = x, Y = y, Count = kv.Value });
- if (x < minx)
- minx = x;
- if (x > maxx)
- maxx = x;
- if (y < miny)
- miny = y;
- if (y > maxy)
- maxy = y;
- if (kv.Value > maxc)
- maxc = kv.Value;
+ if (x < minx) minx = x; if (x > maxx) maxx = x;
+ if (y < miny) miny = y; if (y > maxy) maxy = y;
+ if (kv.Value > maxc) maxc = kv.Value;
}
}
}
- if (double.IsInfinity(minx) || double.IsInfinity(miny))
- {
- minx = 0;
- maxx = 1;
- miny = 0;
- maxy = 1;
- maxc = 1;
- }
- MinX = minx;
- MaxX = maxx;
- MinY = miny;
- MaxY = maxy;
- MaxCount = maxc;
+ if (double.IsInfinity(minx) || double.IsInfinity(miny)) { minx = 0; maxx = 1; miny = 0; maxy = 1; maxc = 1; }
+ MinX = minx; MaxX = maxx; MinY = miny; MaxY = maxy; MaxCount = maxc;
+ HasZeroX = MinX <= 0 && MaxX >= 0; HasZeroY = MinY <= 0 && MaxY >= 0;
+ BuildTicks(XTicks, MinX, MaxX, 6); BuildTicks(YTicks, MinY, MaxY, 6);
}
- private static bool TryGetXY(object key, out double x, out double y)
+ private static void BuildTicks(ObservableCollection target, double min, double max, int desired)
+ {
+ target.Clear(); if (max <= min) { target.Add(new AxisTick { Value = min, Label = min.ToString("G4") }); return; }
+ var range = max - min; var rawStep = range / desired; double magnitude = System.Math.Pow(10, System.Math.Floor(System.Math.Log10(rawStep)));
+ double normalized = rawStep / magnitude; double step = normalized < 1.5 ? 1 * magnitude : normalized < 3 ? 2 * magnitude : normalized < 7 ? 5 * magnitude : 10 * magnitude;
+ double start = System.Math.Ceiling(min / step) * step;
+ for (double v = start; v <= max + step * 0.001; v += step) target.Add(new AxisTick { Value = v, Label = FormatTick(v, range) });
+ if (min <= 0 && max >= 0 && !target.Any(t => System.Math.Abs(t.Value) < step * 0.001)) target.Add(new AxisTick { Value = 0, Label = FormatTick(0, range) });
+ var ordered = target.OrderBy(t => t.Value).ToList(); if (ordered.Count != target.Count) { target.Clear(); foreach (var t in ordered) target.Add(t); }
+ }
+
+ private static string FormatTick(double value, double range)
{
- x = y = 0;
- if (key is null)
- return false;
- var t = key.GetType();
+ if (range == 0) return value.ToString("G4"); var absRange = System.Math.Abs(range);
+ if (absRange >= 100000 || absRange <= 0.001) return value.ToString("G3");
+ if (absRange >= 1000) return value.ToString("G4");
+ if (absRange >= 1) return value.ToString("G5");
+ return value.ToString("G3");
+ }
- //1) Properties named X/Y
- var px = t.GetProperty("X");
- var py = t.GetProperty("Y");
- if (px is not null && py is not null)
+ private static bool TryGetXY(object key, out double x, out double y)
+ {
+ x = y = 0; if (key is null) return false; var t = key.GetType();
+ var px = t.GetProperty("X"); var py = t.GetProperty("Y"); if (px is not null && py is not null)
{
- var vx = px.GetValue(key);
- var vy = py.GetValue(key);
- if (TryToDouble(vx, out x) && TryToDouble(vy, out y))
- return true;
+ var vx = px.GetValue(key); var vy = py.GetValue(key);
+ if (TryToDouble(vx, out x) && TryToDouble(vy, out y)) return true;
}
-
- //2) Fields named X/Y (e.g., System.Numerics.Vector2)
- var fx = t.GetField("X");
- var fy = t.GetField("Y");
- if (fx is not null && fy is not null)
+ var fx = t.GetField("X"); var fy = t.GetField("Y"); if (fx is not null && fy is not null)
{
- var vx = fx.GetValue(key);
- var vy = fy.GetValue(key);
- if (TryToDouble(vx, out x) && TryToDouble(vy, out y))
- return true;
+ var vx = fx.GetValue(key); var vy = fy.GetValue(key);
+ if (TryToDouble(vx, out x) && TryToDouble(vy, out y)) return true;
}
-
- //3) ValueTuple or similar (Item1/Item2)
if (t.IsGenericType && t.FullName!.StartsWith("System.ValueTuple`2"))
{
- var f1 = t.GetField("Item1");
- var f2 = t.GetField("Item2");
- if (f1 is not null && f2 is not null)
+ var f1 = t.GetField("Item1"); var f2 = t.GetField("Item2"); if (f1 is not null && f2 is not null)
{
- var v1 = f1.GetValue(key);
- var v2 = f2.GetValue(key);
- if (TryToDouble(v1, out x) && TryToDouble(v2, out y))
- return true;
+ var v1 = f1.GetValue(key); var v2 = f2.GetValue(key);
+ if (TryToDouble(v1, out x) && TryToDouble(v2, out y)) return true;
}
}
-
- //4) ITuple support
if (key is ITuple tuple && tuple.Length >= 2)
{
- var v1 = tuple[0];
- var v2 = tuple[1];
- if (TryToDouble(v1, out x) && TryToDouble(v2, out y))
- return true;
+ var v1 = tuple[0]; var v2 = tuple[1];
+ if (TryToDouble(v1, out x) && TryToDouble(v2, out y)) return true;
}
-
- //5) double[] or similar length>=2
if (key is System.Collections.IList list && list.Count >= 2)
{
- var v1 = list[0];
- var v2 = list[1];
- if (TryToDouble(v1, out x) && TryToDouble(v2, out y))
- return true;
+ var v1 = list[0]; var v2 = list[1];
+ if (TryToDouble(v1, out x) && TryToDouble(v2, out y)) return true;
}
-
- //6) Fallback: parse from ToString like "(x,y)"
- var s = key.ToString() ?? string.Empty;
- var m = System.Text.RegularExpressions.Regex.Match(s, "\\(?\\s*(-?[0-9]+(?:\\.[0-9]+)?)\\s*,\\s*(-?[0-9]+(?:\\.[0-9]+)?)\\s*\\)?");
+ var s = key.ToString() ?? string.Empty; var m = System.Text.RegularExpressions.Regex.Match(s, @"\(?\s*(-?[0-9]+(?:\.[0-9]+)?)\s*,\s*(-?[0-9]+(?:\.[0-9]+)?)\s*\)?");
if (m.Success)
{
if (double.TryParse(m.Groups[1].Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out x) &&
- double.TryParse(m.Groups[2].Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out y))
- {
- return true;
- }
+ double.TryParse(m.Groups[2].Value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out y)) return true;
}
return false;
}
- private static bool TryToDouble(object? v, out double d)
- {
- try
- {
- d = System.Convert.ToDouble(v, System.Globalization.CultureInfo.InvariantCulture);
- return true;
- }
- catch { d = 0; return false; }
- }
+ private static bool TryToDouble(object? v, out double d) { try { d = System.Convert.ToDouble(v, System.Globalization.CultureInfo.InvariantCulture); return true; } catch { d = 0; return false; } }
}
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Controls/ClusterAggregationView.xaml b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Controls/ClusterAggregationView.xaml
index a46ae7222..f3741c812 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Controls/ClusterAggregationView.xaml
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Controls/ClusterAggregationView.xaml
@@ -1,155 +1,294 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotConverters.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotConverters.cs
index 75433a164..ab2462271 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotConverters.cs
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotConverters.cs
@@ -4,8 +4,10 @@
namespace DataAnalysis.WPF.Views.Converters;
-public sealed class PlotXConverter : IMultiValueConverter
+public sealed class PlotConverter : IMultiValueConverter
{
+ // Half of maximum bubble size (SizeByCountConverter.MaxSize) to keep bubbles inside plot.
+ private const double EdgePadding = 60d; // MaxSize (120) / 2
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values is null || values.Length < 4) return 0d;
@@ -15,9 +17,18 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur
var minX = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture);
var maxX = System.Convert.ToDouble(values[2], CultureInfo.InvariantCulture);
var width = System.Convert.ToDouble(values[3], CultureInfo.InvariantCulture);
- if (maxX <= minX) return 0d;
+ var Offset = parameter != null?System.Convert.ToDouble(parameter, CultureInfo.InvariantCulture):0d;
+ if (width <= 0) return 0d;
+ if (maxX == minX)
+ {
+ // Degenerate range: place at center with padding consideration
+ var usable = Math.Max(0, width - 2 * EdgePadding);
+ return EdgePadding + usable / 2;
+ }
+ var usableWidth = Math.Max(0, width - 2 * EdgePadding);
var t = (x - minX) / (maxX - minX);
- return Math.Max(0, Math.Min(width, t * width));
+ t = Math.Max(0, Math.Min(1, t));
+ return EdgePadding + t * usableWidth+Offset;
}
catch
{
@@ -27,52 +38,3 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException();
}
-
-public sealed class PlotYConverter : IMultiValueConverter
-{
- public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
- {
- if (values is null || values.Length < 4) return 0d;
- try
- {
- var y = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture);
- var minY = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture);
- var maxY = System.Convert.ToDouble(values[2], CultureInfo.InvariantCulture);
- var height = System.Convert.ToDouble(values[3], CultureInfo.InvariantCulture);
- if (maxY <= minY) return 0d;
- var t = (y - minY) / (maxY - minY);
- return Math.Max(0, Math.Min(height, (1 - t) * height));
- }
- catch
- {
- return 0d;
- }
- }
-
- public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException();
-}
-
-public sealed class SizeByCountConverter : IMultiValueConverter
-{
- public double MinSize { get; set; } = 6;
- public double MaxSize { get; set; } = 120;
- public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
- {
- if (values is null || values.Length < 2) return MinSize;
- try
- {
- var count = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture);
- var max = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture);
- if (max <= 0) return MinSize;
- var t = Math.Max(0, Math.Min(1, count / max));
- t = Math.Sqrt(t);
- return MinSize + (MaxSize - MinSize) * t;
- }
- catch
- {
- return MinSize;
- }
- }
-
- public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException();
-}
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotXCenteredConverter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotXCenteredConverter.cs
new file mode 100644
index 000000000..dd5add01d
--- /dev/null
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotXCenteredConverter.cs
@@ -0,0 +1,50 @@
+using System.Globalization;
+using System.Windows.Data;
+
+namespace DataAnalysis.WPF.Views.Converters;
+
+// Centered converters: return Canvas.Left/Top such that given (X,Y) is bubble center.
+// Expect bindings: X|minX|maxX|width|count|maxCount (6 values) for X, similarly Y.
+public sealed class PlotXCenteredConverter : IMultiValueConverter
+{
+ private const double EdgePadding = 60d; // Align with PlotConverter padding
+ private const double MinSize = 6d;
+ private const double MaxSize = 120d;
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values is null || values.Length < 6) return 0d;
+ try
+ {
+ var x = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture);
+ var minX = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture);
+ var maxX = System.Convert.ToDouble(values[2], CultureInfo.InvariantCulture);
+ var width = System.Convert.ToDouble(values[3], CultureInfo.InvariantCulture);
+ var count = System.Convert.ToDouble(values[4], CultureInfo.InvariantCulture);
+ var maxCount = System.Convert.ToDouble(values[5], CultureInfo.InvariantCulture);
+ if (width <= 0) return 0d;
+ double size = MinSize;
+ if (maxCount > 0)
+ {
+ var tSize = Math.Max(0, Math.Min(1, count / maxCount));
+ tSize = Math.Sqrt(tSize);
+ size = MinSize + (MaxSize - MinSize) * tSize;
+ }
+ var radius = size / 2d;
+ if (maxX == minX)
+ {
+ return Math.Max(0, Math.Min(width - size, width / 2d - radius));
+ }
+ var usableWidth = Math.Max(0, width - 2 * EdgePadding);
+ var t = (x - minX) / (maxX - minX);
+ t = Math.Max(0, Math.Min(1, t));
+ var center = EdgePadding + t * usableWidth;
+ var left = center - radius;
+ // Clamp to keep bubble fully visible
+ if (left < 0) left = 0;
+ if (left + size > width) left = Math.Max(0, width - size);
+ return left;
+ }
+ catch { return 0d; }
+ }
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException();
+}
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotYCenteredConverter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotYCenteredConverter.cs
new file mode 100644
index 000000000..1cf1f2607
--- /dev/null
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/PlotYCenteredConverter.cs
@@ -0,0 +1,47 @@
+using System.Globalization;
+using System.Windows.Data;
+
+namespace DataAnalysis.WPF.Views.Converters;
+
+public sealed class PlotYCenteredConverter : IMultiValueConverter
+{
+ private const double EdgePadding = 60d;
+ private const double MinSize = 6d;
+ private const double MaxSize = 120d;
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values is null || values.Length < 6) return 0d;
+ try
+ {
+ var y = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture);
+ var minY = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture);
+ var maxY = System.Convert.ToDouble(values[2], CultureInfo.InvariantCulture);
+ var height = System.Convert.ToDouble(values[3], CultureInfo.InvariantCulture);
+ var count = System.Convert.ToDouble(values[4], CultureInfo.InvariantCulture);
+ var maxCount = System.Convert.ToDouble(values[5], CultureInfo.InvariantCulture);
+ if (height <= 0) return 0d;
+ double size = MinSize;
+ if (maxCount > 0)
+ {
+ var tSize = Math.Max(0, Math.Min(1, count / maxCount));
+ tSize = Math.Sqrt(tSize);
+ size = MinSize + (MaxSize - MinSize) * tSize;
+ }
+ var radius = size / 2d;
+ if (maxY <= minY)
+ {
+ return Math.Max(0, Math.Min(height - size, height / 2d - radius));
+ }
+ var usableHeight = Math.Max(0, height - 2 * EdgePadding);
+ var t = (y - minY) / (maxY - minY);
+ t = Math.Max(0, Math.Min(1, t));
+ var center = EdgePadding + (1 - t) * usableHeight; // invert y
+ var top = center - radius;
+ if (top < 0) top = 0;
+ if (top + size > height) top = Math.Max(0, height - size);
+ return top;
+ }
+ catch { return 0d; }
+ }
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException();
+}
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/SizeByCountConverter.cs b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/SizeByCountConverter.cs
new file mode 100644
index 000000000..1671dd3da
--- /dev/null
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.WPF/Views/Converters/SizeByCountConverter.cs
@@ -0,0 +1,25 @@
+using System.Globalization;
+using System.Windows.Data;
+
+namespace DataAnalysis.WPF.Views.Converters;
+
+public sealed class SizeByCountConverter : IMultiValueConverter
+{
+ public double MinSize { get; set; } = 6;
+ public double MaxSize { get; set; } = 120;
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values is null || values.Length < 2) return MinSize;
+ try
+ {
+ var count = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture);
+ var max = System.Convert.ToDouble(values[1], CultureInfo.InvariantCulture);
+ if (max <= 0) return MinSize;
+ var t = Math.Max(0, Math.Min(1, count / max));
+ t = Math.Sqrt(t);
+ return MinSize + (MaxSize - MinSize) * t;
+ }
+ catch { return MinSize; }
+ }
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException();
+}
diff --git a/CSharpBible/Data/DataAnalysis/DataAnalysis.sln b/CSharpBible/Data/DataAnalysis/DataAnalysis.sln
index 3a5acb01e..ddc2dea06 100644
--- a/CSharpBible/Data/DataAnalysis/DataAnalysis.sln
+++ b/CSharpBible/Data/DataAnalysis/DataAnalysis.sln
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.14.36616.10
+# Visual Studio Version 18
+VisualStudioVersion = 18.0.11205.157 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAnalysis.Core", "DataAnalysis.Core\DataAnalysis.Core.csproj", "{D8808BEF-2D64-4D46-903F-B1A0499D9F66}"
EndProject
@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAnalysis.WPF.TestHarnes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataConvert.Console", "DataConvert.Console\DataConvert.Console.csproj", "{E9D94D2B-42BA-4AD6-9CB2-D2D889B289FE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAnalysis.Core.Tests", "DataAnalysis.Core.Tests\DataAnalysis.Core.Tests.csproj", "{DB99A65F-675C-E212-80B3-50A0C0D87138}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -39,6 +41,10 @@ Global
{E9D94D2B-42BA-4AD6-9CB2-D2D889B289FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9D94D2B-42BA-4AD6-9CB2-D2D889B289FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9D94D2B-42BA-4AD6-9CB2-D2D889B289FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DB99A65F-675C-E212-80B3-50A0C0D87138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DB99A65F-675C-E212-80B3-50A0C0D87138}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DB99A65F-675C-E212-80B3-50A0C0D87138}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DB99A65F-675C-E212-80B3-50A0C0D87138}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/App.cs b/CSharpBible/Data/DataAnalysis/DataConvert.Console/App.cs
index 90c015545..53d2923d0 100644
--- a/CSharpBible/Data/DataAnalysis/DataConvert.Console/App.cs
+++ b/CSharpBible/Data/DataAnalysis/DataConvert.Console/App.cs
@@ -18,6 +18,7 @@ public App()
{
// Exporter: table only
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton(sp => new DelimitedTableParsingProfile
{
@@ -44,6 +45,8 @@ public App()
.Build();
}
+ IConsoleWriter? console;
+
public async Task RunAsync(string[] args)
{
await _host.StartAsync();
@@ -57,10 +60,12 @@ public async Task RunAsync(string[] args)
var inputPath = args[0];
var tableReader = _host.Services.GetRequiredService();
var exporter = _host.Services.GetRequiredService();
-
- var table = await tableReader.ReadTableAsync(inputPath, CancellationToken.None);
- var output = await exporter.ExportAsync(table, inputPath, null, CancellationToken.None);
- System.Console.WriteLine(output);
+ console = _host.Services.GetRequiredService();
+ console.WriteLine("Reading ...");
+ var table = await tableReader.ReadTableAsync(inputPath, CancellationToken.None,onProgress);
+ console.WriteLine("\nConverting ...");
+ var output = await exporter.ExportAsync(table, inputPath, null, CancellationToken.None,onProgress);
+ console.WriteLine("\n"+output);
return 0;
}
catch (Exception ex)
@@ -74,4 +79,14 @@ public async Task RunAsync(string[] args)
_host.Dispose();
}
}
+
+ private void onProgress(double obj)
+ {
+ console.Write("[");
+ int totalBlocks = 70;
+ int filledBlocks = (int)(obj * totalBlocks);
+ console.Write(new string('#', filledBlocks));
+ console.Write(new string('-', totalBlocks - filledBlocks));
+ console.Write($"] {obj:P0}\r");
+ }
}
diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/ConsoleWriter.cs b/CSharpBible/Data/DataAnalysis/DataConvert.Console/ConsoleWriter.cs
new file mode 100644
index 000000000..f5aa8739c
--- /dev/null
+++ b/CSharpBible/Data/DataAnalysis/DataConvert.Console/ConsoleWriter.cs
@@ -0,0 +1,14 @@
+namespace DataConvert.Console;
+
+public class ConsoleWriter : IConsoleWriter
+{
+ public void Write(string v)
+ {
+ System.Console.Write(v);
+ }
+
+ public void WriteLine(string v)
+ {
+ System.Console.WriteLine(v);
+ }
+}
\ No newline at end of file
diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/DataConvert.Console.csproj b/CSharpBible/Data/DataAnalysis/DataConvert.Console/DataConvert.Console.csproj
index 70799f400..6af1c927c 100644
--- a/CSharpBible/Data/DataAnalysis/DataConvert.Console/DataConvert.Console.csproj
+++ b/CSharpBible/Data/DataAnalysis/DataConvert.Console/DataConvert.Console.csproj
@@ -7,6 +7,10 @@
enable
+
+
+
+
@@ -15,4 +19,10 @@
+
+
+ PreserveNewest
+
+
+
diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/IConsoleWriter.cs b/CSharpBible/Data/DataAnalysis/DataConvert.Console/IConsoleWriter.cs
new file mode 100644
index 000000000..414f7e8f5
--- /dev/null
+++ b/CSharpBible/Data/DataAnalysis/DataConvert.Console/IConsoleWriter.cs
@@ -0,0 +1,7 @@
+namespace DataConvert.Console;
+
+internal interface IConsoleWriter
+{
+ void Write(string v);
+ void WriteLine(string v);
+}
\ No newline at end of file
diff --git a/CSharpBible/Data/DataAnalysis/DataConvert.Console/Ressources/.info b/CSharpBible/Data/DataAnalysis/DataConvert.Console/Ressources/.info
new file mode 100644
index 000000000..e69de29bb
diff --git a/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEdit.sln b/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEdit.sln
index b533bdb3f..bd78c49ad 100644
--- a/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEdit.sln
+++ b/CSharpBible/MVVM_Tutorial/MVVM_25_RichTextEdit.sln
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0
+# Visual Studio Version 18
+VisualStudioVersion = 18.0.11205.157 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projektmappenelemente", "Projektmappenelemente", "{658BD492-33FF-4995-AAE7-4F6894E92F0C}"
ProjectSection(SolutionItems) = preProject
@@ -30,6 +30,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonDialogs", "..\Librari
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonDialogs_net", "..\Libraries\CommonDialogs\CommonDialogs_net.csproj", "{7CF63553-BD9F-4999-B6D8-669405EA230E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MVVM_25a_CTRichTextEdit", "MVVM_25_CTRichTextEdit\MVVM_25a_CTRichTextEdit.csproj", "{83BA34EF-0E3B-A999-B15B-8757CD718FF0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MVVM_25a_CTRichTextEdit_net", "MVVM_25_CTRichTextEdit\MVVM_25a_CTRichTextEdit_net.csproj", "{2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -102,6 +106,22 @@ Global
{7CF63553-BD9F-4999-B6D8-669405EA230E}.Release|Any CPU.Build.0 = Release|Any CPU
{7CF63553-BD9F-4999-B6D8-669405EA230E}.Release|x86.ActiveCfg = Release|x86
{7CF63553-BD9F-4999-B6D8-669405EA230E}.Release|x86.Build.0 = Release|x86
+ {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Debug|x86.Build.0 = Debug|Any CPU
+ {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Release|x86.ActiveCfg = Release|Any CPU
+ {83BA34EF-0E3B-A999-B15B-8757CD718FF0}.Release|x86.Build.0 = Release|Any CPU
+ {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Debug|x86.Build.0 = Debug|Any CPU
+ {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Release|x86.ActiveCfg = Release|Any CPU
+ {2946216E-E3F5-CDF0-45EC-A1B03BD3BC9B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/TestStatements/AppWithPlugin/AppWithPlugin.csproj b/TestStatements/AppWithPlugin/AppWithPlugin.csproj
index 70348bf04..c19edbe79 100644
--- a/TestStatements/AppWithPlugin/AppWithPlugin.csproj
+++ b/TestStatements/AppWithPlugin/AppWithPlugin.csproj
@@ -19,9 +19,9 @@
-
+
-
+
diff --git a/TestStatements/AppWithPluginWpf/AppWithPluginWpf.csproj b/TestStatements/AppWithPluginWpf/AppWithPluginWpf.csproj
index d64900ed4..07f0839ae 100644
--- a/TestStatements/AppWithPluginWpf/AppWithPluginWpf.csproj
+++ b/TestStatements/AppWithPluginWpf/AppWithPluginWpf.csproj
@@ -17,9 +17,9 @@
-
+
-
+
diff --git a/TestStatements/AsyncExampleWPF/README.md b/TestStatements/AsyncExampleWPF/README.md
new file mode 100644
index 000000000..47f5fe5eb
--- /dev/null
+++ b/TestStatements/AsyncExampleWPF/README.md
@@ -0,0 +1,22 @@
+# AsyncExampleWPF
+
+Multi-targeted WPF sample application (net462; net472; net48; net481) demonstrating asynchronous programming patterns in a desktop UI context.
+
+## Goals
+- Showcase async/await integration with WPF message loop
+- Illustrate responsiveness improvements vs synchronous blocking
+- Provide a baseline for comparing legacy framework behaviors
+
+## Target Frameworks
+Classic .NET Framework TFMs only; no .NET (Core) targets—useful to contrast with newer projects in the solution.
+
+## Features (expected in code-behind / ViewModels)
+- Async event handlers (e.g., button click triggering background operations)
+- UI thread marshaling via SynchronizationContext / Dispatcher
+- Potential cancellation token usage
+
+## Build
+`dotnet build AsyncExampleWPF/AsyncExampleWPF.csproj`
+
+## Extensibility
+Project can be upgraded to multi-target .NET 6+ WPF by adding windows TFMs (e.g., net8.0-windows) if desired.
diff --git a/TestStatements/HelloPlugin/HelloPlugin.csproj b/TestStatements/HelloPlugin/HelloPlugin.csproj
index 5de2be730..55d3b5677 100644
--- a/TestStatements/HelloPlugin/HelloPlugin.csproj
+++ b/TestStatements/HelloPlugin/HelloPlugin.csproj
@@ -19,7 +19,7 @@
-
+
diff --git a/TestStatements/HelloPluginTest/HelloPluginTest.csproj b/TestStatements/HelloPluginTest/HelloPluginTest.csproj
index 037dd011e..62eed8838 100644
--- a/TestStatements/HelloPluginTest/HelloPluginTest.csproj
+++ b/TestStatements/HelloPluginTest/HelloPluginTest.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/TestStatements/OtherPlugin/OtherPlugin.csproj b/TestStatements/OtherPlugin/OtherPlugin.csproj
index cbb3107bf..78325ffb4 100644
--- a/TestStatements/OtherPlugin/OtherPlugin.csproj
+++ b/TestStatements/OtherPlugin/OtherPlugin.csproj
@@ -15,7 +15,7 @@
..\snKey.snk
-
+
diff --git a/TestStatements/TestStatements/TestStatements.csproj b/TestStatements/TestStatements/TestStatements.csproj
index a1e1a41ed..9bcc4a375 100644
--- a/TestStatements/TestStatements/TestStatements.csproj
+++ b/TestStatements/TestStatements/TestStatements.csproj
@@ -56,10 +56,10 @@
-
+
-
-
+
+
diff --git a/TestStatements/TestStatements/TestStatements_net.csproj b/TestStatements/TestStatements/TestStatements_net.csproj
index bd7a142fd..9174fd38b 100644
--- a/TestStatements/TestStatements/TestStatements_net.csproj
+++ b/TestStatements/TestStatements/TestStatements_net.csproj
@@ -56,9 +56,9 @@
-
-
-
+
+
+
diff --git a/Transpiler_pp/Analyzer1/Analyzer1.CodeFixes/Analyzer1.CodeFixes.csproj b/Transpiler_pp/Analyzer1/Analyzer1.CodeFixes/Analyzer1.CodeFixes.csproj
index 31864fc10..23ffa980a 100644
--- a/Transpiler_pp/Analyzer1/Analyzer1.CodeFixes/Analyzer1.CodeFixes.csproj
+++ b/Transpiler_pp/Analyzer1/Analyzer1.CodeFixes/Analyzer1.CodeFixes.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/Transpiler_pp/Analyzer1/Analyzer1/Analyzer1.csproj b/Transpiler_pp/Analyzer1/Analyzer1/Analyzer1.csproj
index 6d1da0b9c..f57f4112d 100644
--- a/Transpiler_pp/Analyzer1/Analyzer1/Analyzer1.csproj
+++ b/Transpiler_pp/Analyzer1/Analyzer1/Analyzer1.csproj
@@ -15,7 +15,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/Transpiler_pp/Transpiler.sln b/Transpiler_pp/Transpiler.sln
index 64a695415..625adcb41 100644
--- a/Transpiler_pp/Transpiler.sln
+++ b/Transpiler_pp/Transpiler.sln
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
-VisualStudioVersion = 18.0.11201.2 d18.0
+VisualStudioVersion = 18.0.11201.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projektmappenelemente", "Projektmappenelemente", "{FB9836CD-815C-4901-B10D-8CCF99B18E30}"
ProjectSection(SolutionItems) = preProject
@@ -30,14 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer1.CodeFixes", "Anal
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer1.Package", "Analyzer1\Analyzer1.Package\Analyzer1.Package.csproj", "{15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer1.Test", "Analyzer1\Analyzer1.Test\Analyzer1.Test.csproj", "{29649B15-C865-4DFF-A2C6-D4A396DF9578}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer1.Vsix", "Analyzer1\Analyzer1.Vsix\Analyzer1.Vsix.csproj", "{8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VBUnObfusicator", "..\Gen_FreeWin\VBUnObfusicator\VBUnObfusicator.csproj", "{4D2FE91E-8018-4CB7-9778-5648833E883D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VBUnObfusicatorTests", "..\Gen_FreeWin\VBUnObfusicatorTests\VBUnObfusicatorTests.csproj", "{EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TranspilerLibTests", "TranspilerLibTests\TranspilerLibTests.csproj", "{C87BEC24-2C21-4DEF-BB8D-B9503105A086}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseLib", "..\CSharpBible\Libraries\BaseLib\BaseLib.csproj", "{8608004D-250B-461E-BD95-D288414A9C01}"
@@ -52,6 +46,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TranspilerLib.CSharp", "Tra
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TranspilerLib.Pascal.Tests", "TranspilerLib.Pascal.Tests\TranspilerLib.Pascal.Tests.csproj", "{DD21FC5F-2FFF-4BD6-847B-7B5006DB3389}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cmd", "cmd", "{0E2C6B7C-A659-45E8-AD73-092473A0572D}"
+ ProjectSection(SolutionItems) = preProject
+ ..\..\Cmd\CheckIn2.cmd = ..\..\Cmd\CheckIn2.cmd
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -146,18 +145,6 @@ Global
{15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD}.Release|x86.ActiveCfg = Release|Any CPU
{15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD}.Release|x86.Build.0 = Release|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|x86.ActiveCfg = Debug|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Debug|x86.Build.0 = Debug|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|Any CPU.Build.0 = Release|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|Mixed Platforms.Build.0 = Release|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|x86.ActiveCfg = Release|Any CPU
- {29649B15-C865-4DFF-A2C6-D4A396DF9578}.Release|x86.Build.0 = Release|Any CPU
{8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@@ -170,30 +157,6 @@ Global
{8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Release|x86.ActiveCfg = Release|Any CPU
{8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0}.Release|x86.Build.0 = Release|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|x86.ActiveCfg = Debug|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Debug|x86.Build.0 = Debug|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|Any CPU.Build.0 = Release|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|x86.ActiveCfg = Release|Any CPU
- {4D2FE91E-8018-4CB7-9778-5648833E883D}.Release|x86.Build.0 = Release|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|x86.ActiveCfg = Debug|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Debug|x86.Build.0 = Debug|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|Any CPU.Build.0 = Release|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|x86.ActiveCfg = Release|Any CPU
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD}.Release|x86.Build.0 = Release|Any CPU
{C87BEC24-2C21-4DEF-BB8D-B9503105A086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C87BEC24-2C21-4DEF-BB8D-B9503105A086}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C87BEC24-2C21-4DEF-BB8D-B9503105A086}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@@ -290,10 +253,7 @@ Global
{68CCC629-61B7-435B-A39C-C9EE023388D6} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E}
{BE43CB35-C6AA-4EA3-BE1D-8DC865B948A8} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E}
{15FE2AB9-637E-4FD2-BB48-67C3CE22DEBD} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E}
- {29649B15-C865-4DFF-A2C6-D4A396DF9578} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E}
{8A24ABD1-0D57-4EFD-8C2A-4999D4DACAE0} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E}
- {4D2FE91E-8018-4CB7-9778-5648833E883D} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E}
- {EE28DDFE-03E7-4F9F-9CAB-88F6092983FD} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E}
{C87BEC24-2C21-4DEF-BB8D-B9503105A086} = {5481B009-5D62-45A8-9DD3-B9E6792B0C0E}
{8608004D-250B-461E-BD95-D288414A9C01} = {8655F273-B5AF-4D9A-B635-7F9168703579}
{893755DA-9A1A-4DCB-8B7D-CD5C61790B76} = {8655F273-B5AF-4D9A-B635-7F9168703579}