using File_Folder_Helper.Models; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Globalization; namespace File_Folder_Helper.Helpers; internal static partial class HelperGenealogicalDataCommunication { private record Input(string People, string? GenealogicalDataCommunicationFile, string? GenealogicalDataCommunicationDirectory, string? Destination); private record GenealogicalDataCommunicationCollections(string[] HeaderLines, ReadOnlyDictionary IndividualsLines, List FamilyGroupLines, ReadOnlyDictionary PersonKeyToId, string[] FooterLines); private record PersonHour(char Status, char Sex, char First); private record PersonName(string? First, string? Middle, string? Last, string? Alias); private record PersonDirectory(string[] DirectoryNames, DateTime? Birthday, int? ApproximateYears, PersonHour? PersonHour, PersonName? PersonName, string[]? BirthdayNotes, string[]? Notes); private static Input GetInput(List args) { Input result; string? destination = null; string? genealogicalDataCommunicationFile = null; string? genealogicalDataCommunicationDirectory = null; for (int i = 1; i < args.Count; i++) { if (args[i].Length == 2 && i + 1 < args.Count) { if (args[i][1] == 'g') genealogicalDataCommunicationFile = Path.GetFullPath(args[i + 1]); else if (args[i][1] == 'd') destination = Path.GetFullPath(args[i + 1]); i++; } } if (genealogicalDataCommunicationFile is not null) { string? root = Path.GetPathRoot(genealogicalDataCommunicationFile); if (root is null || !Directory.Exists(root)) throw new NotSupportedException($"This method requires valid -g path <{root}>!"); if (!File.Exists(genealogicalDataCommunicationFile)) throw new NotSupportedException($"This method requires valid -g path <{root}>!"); string? directoryName = Path.GetDirectoryName(genealogicalDataCommunicationFile) ?? throw new NotSupportedException($"This method requires valid -g path <{root}>!"); genealogicalDataCommunicationDirectory = Path.Combine(directoryName, Path.GetFileNameWithoutExtension(genealogicalDataCommunicationFile)); if (!Directory.Exists(genealogicalDataCommunicationDirectory)) _ = Directory.CreateDirectory(genealogicalDataCommunicationDirectory); } if (destination is not null) { string? root = Path.GetPathRoot(destination); if (root is null || !Directory.Exists(root)) throw new NotSupportedException($"This method requires frontMatterYamlLines valid -d path <{root}>!"); if (!Directory.Exists(destination)) _ = Directory.CreateDirectory(destination); } result = new(Path.GetFullPath(args.First()), genealogicalDataCommunicationFile, genealogicalDataCommunicationDirectory, destination); return result; } private static string[] GetHeaderLines(string startsWith, string[] sourceLines) { List results = new(); for (int i = 0; i < sourceLines.Length; i++) { if (sourceLines[i].StartsWith(startsWith)) break; results.Add(sourceLines[i]); } return results.ToArray(); } private static ReadOnlyDictionary Convert(Dictionary> keyValuePairs) { Dictionary results = new(); foreach (KeyValuePair> keyValuePair in keyValuePairs) results.Add(keyValuePair.Key, keyValuePair.Value.ToArray()); return new(results); } private static GenealogicalDataCommunicationCollections GetGenealogicalDataCommunicationCollections(AppSettings appSettings, Input input) { GenealogicalDataCommunicationCollections result; long? personKey; string[] segments; string personKeyFormatted; List lines = new(); const string startsWith = "0 @"; List footerLines = new(); List familyGroupLines = new(); Dictionary personKeyToId = new(); ReadOnlyDictionary individualsLines; Dictionary> keyValuePairs = new(); int length = appSettings.PersonBirthdayFormat.Length; string[] sourceLines = string.IsNullOrEmpty(input.GenealogicalDataCommunicationFile) || !File.Exists(input.GenealogicalDataCommunicationFile) ? Array.Empty() : File.ReadAllLines(input.GenealogicalDataCommunicationFile); string[] headerLines = GetHeaderLines(startsWith, sourceLines); for (int i = headerLines.Length; i < sourceLines.Length; i++) { if (!sourceLines[i].StartsWith(startsWith)) continue; lines.Add(sourceLines[i]); if (sourceLines[i].EndsWith("@ SOUR") || sourceLines[i].EndsWith("@ SUBM") || sourceLines[i].EndsWith("@ OBJE") || sourceLines[i].EndsWith("@ REPO")) continue; else if (sourceLines[i].EndsWith("@ FAM")) { lines.Clear(); for (int j = sourceLines.Length - 1; j >= i; j--) { lines.Add(sourceLines[j]); if (sourceLines[j].First() == '0') { if (!sourceLines[j].EndsWith("@ FAM")) footerLines.AddRange(lines); else { lines.Reverse(); familyGroupLines.Add(lines.ToArray()); } lines.Clear(); } } familyGroupLines.Reverse(); footerLines.Reverse(); break; } else if (sourceLines[i].EndsWith("@ INDI")) { personKey = null; segments = sourceLines[i].Split('@'); for (int j = i + 1; j < sourceLines.Length; j++) { if (sourceLines[j].StartsWith(startsWith)) break; lines.Add(sourceLines[j]); if (sourceLines[j].StartsWith("2 NICK ")) personKeyFormatted = sourceLines[j][7..]; else if (sourceLines[j] == "1 BIRT" && sourceLines[j + 1].StartsWith("2 DATE") && sourceLines[j + 2].StartsWith("2 NOTE ")) personKeyFormatted = sourceLines[j + 2][7..]; else continue; if (segments.Length != 3 || personKeyFormatted.Length != length) continue; if (!DateTime.TryParseExact(personKeyFormatted, appSettings.PersonBirthdayFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dateTime)) continue; personKey = dateTime.Ticks; if (segments[1].Length < 2 || !int.TryParse(segments[1][1..], out int id)) continue; personKeyToId.Add(personKey.Value, id); } if (personKey is null) throw new Exception(string.Join(Environment.NewLine, lines)); keyValuePairs.Add(personKey.Value, new()); if (!lines.Any()) continue; keyValuePairs[personKey.Value].AddRange(lines); lines.Clear(); } else throw new NotSupportedException(); } individualsLines = Convert(keyValuePairs); result = new(headerLines, individualsLines, familyGroupLines, new(personKeyToId), footerLines.ToArray()); return result; } private static List GetDirectoryNames(string directory) { List results = new(); string? fileName; string? checkDirectory = directory; string? pathRoot = Path.GetPathRoot(directory); string extension = Path.GetExtension(directory); if (string.IsNullOrEmpty(pathRoot)) throw new NullReferenceException(nameof(pathRoot)); if (Directory.Exists(directory)) { fileName = Path.GetFileName(directory); if (!string.IsNullOrEmpty(fileName)) results.Add(fileName); } else if ((string.IsNullOrEmpty(extension) || extension.Length > 3) && !File.Exists(directory)) { fileName = Path.GetFileName(directory); if (!string.IsNullOrEmpty(fileName)) results.Add(fileName); } for (int i = 0; i < int.MaxValue; i++) { checkDirectory = Path.GetDirectoryName(checkDirectory); if (string.IsNullOrEmpty(checkDirectory) || checkDirectory == pathRoot) break; fileName = Path.GetFileName(checkDirectory); if (string.IsNullOrEmpty(fileName)) continue; results.Add(fileName); } results.Add(pathRoot); results.Reverse(); return results; } private static List GetPersonDirectoriesNull(AppSettings appSettings, Input input) { List results = new(); DateTime? birthday; string directoryName; string subDirectoryName; List directoryNames; IEnumerable subDirectories; int length = appSettings.PersonBirthdayFormat.Length; string[] directories = Directory.GetDirectories(input.People, "*", SearchOption.TopDirectoryOnly); foreach (string directory in directories) { directoryName = Path.GetFileName(directory); if (directoryName.Length != 1 || !appSettings.PersonCharacters.Any(l => directoryName.First() == l)) continue; subDirectories = Directory.GetDirectories(directory, "*", SearchOption.AllDirectories); foreach (string subDirectory in subDirectories) { subDirectoryName = Path.GetFileName(subDirectory); if (subDirectoryName.Length != length) continue; if (subDirectoryName.First() is not '1' and not '2') continue; directoryNames = GetDirectoryNames(subDirectory); birthday = !DateTime.TryParseExact(subDirectoryName, appSettings.PersonBirthdayFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dateTime) ? null : dateTime; results.Add(new(directoryNames.ToArray(), birthday, null, null, null, null, null)); } } return results; } private static PersonName GetPersonName(string name) { PersonName result; string? personNameLast; string? personNameAlias; string? personNameFirst; string? personNameMiddle; string[] segmentsA = name.Split('('); string[] segmentsB = segmentsA.First().Trim().Split(' '); if (segmentsB.Length == 1) { personNameFirst = null; personNameLast = null; personNameMiddle = null; personNameAlias = segmentsA.Length == 1 ? segmentsB.First() : segmentsA.Last().Split(')').First(); } else { if (segmentsB.Length == 2) { personNameFirst = segmentsB.First(); personNameLast = segmentsB.Last(); personNameMiddle = null; } else if (segmentsB.Length == 3) { personNameFirst = segmentsB.First(); personNameLast = segmentsB.Last(); personNameMiddle = segmentsB[1]; } else { personNameFirst = segmentsB.First(); personNameLast = string.Join(' ', segmentsB, 1, segmentsB.Length - 1); personNameMiddle = null; } personNameAlias = segmentsA.Length == 1 ? null : segmentsA.Last().Split(')').First(); } result = new(personNameFirst, personNameMiddle, personNameLast, personNameAlias); return result; } private static PersonHour GetPersonHour(string personDisplayDirectoryName, int hour) => hour == 0 ? new('U', 'U', 'U') : hour == 1 ? new('U', 'U', 'U') : hour == 2 ? new('U', 'U', 'U') : hour == 3 ? new('A', 'U', 'Y') : hour == 4 ? new('A', 'F', 'Y') : hour == 5 ? new('A', 'M', 'Y') : hour == 6 ? new('A', 'F', 'N') : hour == 7 ? new('A', 'M', 'N') : hour == 13 ? new('D', 'U', 'Y') : hour == 14 ? new('D', 'F', 'Y') : hour == 15 ? new('D', 'M', 'Y') : hour == 16 ? new('D', 'F', 'N') : hour == 17 ? new('D', 'M', 'N') : throw new NotImplementedException(personDisplayDirectoryName); private static string GetHourGroup(string personDisplayDirectoryName, int hour) => hour == 0 ? "Unknown-Unknown-Unknown" : hour == 1 ? "Unknown-Unknown-Unknown" : hour == 2 ? "Unknown-Unknown-Unknown" : hour == 3 ? "Alive-Unknown-Yes" : hour == 4 ? "Alive-Female-Yes" : hour == 5 ? "Alive-Male-Yes" : hour == 6 ? "Alive-Female-No" : hour == 7 ? "Alive-Male-No" : hour == 13 ? "Dead-Unknown-Yes" : hour == 14 ? "Dead-Female-Yes" : hour == 15 ? "Dead-Male-Yes" : hour == 16 ? "Dead-Female-No" : hour == 17 ? "Dead-Male-No" : throw new NotImplementedException(personDisplayDirectoryName); private static List GetNotes(PersonDirectory personDirectory) { List results = new(); string directory = string.Join(Path.DirectorySeparatorChar, personDirectory.DirectoryNames); if (Directory.Exists(directory)) { string[] lines; string[] files = Directory.GetFiles(directory, "*", SearchOption.AllDirectories); foreach (string file in files) { if (file.EndsWith(".lnk") || file.EndsWith(".rel") || file.EndsWith(".pged") || file.EndsWith(".gif") || file.EndsWith(".jpg") || file.EndsWith(".png")) continue; lines = File.ReadAllLines(file); if (lines.Length > 0 && lines.First().StartsWith("0 @I")) continue; if (!lines.Any(l => l.StartsWith("http"))) continue; results.AddRange(lines.Where(l => !string.IsNullOrEmpty(l))); } } return results; } private static string GetApproximateYears(AppSettings appSettings, string personDisplayDirectoryName) => $"{appSettings.PersonCharacters.Where(l => personDisplayDirectoryName.Contains(l)).FirstOrDefault()}{personDisplayDirectoryName.Split(appSettings.PersonCharacters).Last()}"; private static List GetPersonDirectories(AppSettings appSettings, ReadOnlyDictionary> keyValuePairs) { List results = new(); PersonDirectory[] sorted; List notes = new(); PersonDirectory personDirectory; List birthdayNotes = new(); foreach (KeyValuePair> keyValuePair in keyValuePairs) { if (keyValuePair.Value.Count == 0) throw new NotSupportedException(); sorted = (from l in keyValuePair.Value orderby l.Birthday is null, l.Birthday descending select l).ToArray(); personDirectory = sorted.First(); if (personDirectory.Birthday is null) continue; for (int i = 0; i < sorted.Length; i++) { notes.AddRange(GetNotes(sorted[i])); if (i == 0) birthdayNotes.Add(GetApproximateYears(appSettings, sorted[i].DirectoryNames[^2])); if (i > 0) birthdayNotes.Add(sorted[i].DirectoryNames.Last()); if (i == sorted.Length - 1) { results.Add(new(personDirectory.DirectoryNames, personDirectory.Birthday, personDirectory.ApproximateYears, personDirectory.PersonHour, personDirectory.PersonName, birthdayNotes.ToArray(), notes.ToArray())); notes.Clear(); birthdayNotes.Clear(); } } } return results; } private static ReadOnlyDictionary> Convert(List collection) { Dictionary> results = new(); string personDisplayDirectoryName; List? personDirectories; foreach (PersonDirectory personDirectory in collection) { personDisplayDirectoryName = personDirectory.DirectoryNames[^2]; if (!results.TryGetValue(personDisplayDirectoryName, out personDirectories)) { results.Add(personDisplayDirectoryName, new()); if (!results.TryGetValue(personDisplayDirectoryName, out personDirectories)) throw new NotSupportedException(); } personDirectories.Add(personDirectory); } return new(results); } private static ReadOnlyCollection GetPersonDirectories(AppSettings appSettings, Input input) { List results; string[] segments; PersonName personName; PersonHour? personHour; string personDisplayDirectoryName; List collection = new(); List personDirectories = GetPersonDirectoriesNull(appSettings, input); foreach (PersonDirectory personDirectory in personDirectories) { if (personDirectory.DirectoryNames.Length < 3) continue; personDisplayDirectoryName = personDirectory.DirectoryNames[^2]; segments = personDisplayDirectoryName.Split(appSettings.PersonCharacters); if (segments.Length == 1 || !int.TryParse(segments.Last().Split('-').First(), out int years)) continue; personName = GetPersonName(segments.First()); personHour = personDirectory.Birthday is null ? null : GetPersonHour(personDisplayDirectoryName, personDirectory.Birthday.Value.Hour); collection.Add(new(personDirectory.DirectoryNames, personDirectory.Birthday, years, personHour, personName, null, null)); } ReadOnlyDictionary> keyValuePairs = Convert(collection); results = GetPersonDirectories(appSettings, keyValuePairs); return new(results); } private static Dictionary GetCopyOfPersonKeyToId(GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections) { Dictionary results = new(); foreach (KeyValuePair keyValuePair in genealogicalDataCommunicationCollections.PersonKeyToId) results.Add(keyValuePair.Key, keyValuePair.Value); return results; } private static ReadOnlyDictionary GetIndividualsLines(AppSettings appSettings, GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections, ReadOnlyCollection personDirectories) { Dictionary results = new(); int id; string suffix; string segmentsFirst; List lines = new(); string personDisplayDirectoryName; Dictionary copyOfPersonKeyToId = GetCopyOfPersonKeyToId(genealogicalDataCommunicationCollections); int maxId = copyOfPersonKeyToId.Count == 0 ? 1 : copyOfPersonKeyToId.Values.Max(); foreach (PersonDirectory personDirectory in personDirectories) { lines.Clear(); if (personDirectory.Birthday is null || personDirectory.PersonName is null) continue; if (personDirectory.DirectoryNames.Length < 3) continue; if (!string.IsNullOrEmpty(personDirectory.PersonName.Alias) && personDirectory.PersonName.Alias.Contains("Jr")) suffix = " Jr"; else if (!string.IsNullOrEmpty(personDirectory.PersonName.Alias) && personDirectory.PersonName.Alias.Contains("Sr")) suffix = " Sr"; else suffix = string.Empty; if (copyOfPersonKeyToId.TryGetValue(personDirectory.Birthday.Value.Ticks, out id)) _ = copyOfPersonKeyToId.Remove(personDirectory.Birthday.Value.Ticks); else { maxId += 1; id = maxId; } personDisplayDirectoryName = personDirectory.DirectoryNames[^2]; segmentsFirst = personDisplayDirectoryName.Split(appSettings.PersonCharacters).First(); lines.Add($"0 @I{id}@ INDI"); lines.Add($"1 NAME {personDirectory.PersonName.First} /{personDirectory.PersonName.Last}/{suffix}"); if (!string.IsNullOrEmpty(personDirectory.PersonName.First)) lines.Add($"2 GIVN {personDirectory.PersonName.First}"); if (!string.IsNullOrEmpty(personDirectory.PersonName.Last)) lines.Add($"2 SURN {personDirectory.PersonName.Last}"); if (!string.IsNullOrEmpty(suffix)) lines.Add($"2 NSFX {suffix.Trim()}"); if ($"{personDirectory.PersonName.First} {personDirectory.PersonName.Last}" != segmentsFirst) lines.Add($"2 NICK {segmentsFirst}"); if (personDirectory.PersonHour is not null && personDirectory.PersonHour.Sex != 'U') lines.Add($"1 SEX {personDirectory.PersonHour.Sex}"); lines.Add($"1 _UID {personDirectory.Birthday.Value.Ticks}"); lines.Add("1 BIRT"); lines.Add($"2 DATE {personDirectory.Birthday.Value:d MMM yyyy}".ToUpper()); lines.Add($"2 NOTE {personDirectory.Birthday.Value.ToString(appSettings.PersonBirthdayFormat)}"); if (personDirectory.BirthdayNotes is not null) { foreach (string birthdayNote in personDirectory.BirthdayNotes) lines.Add($"3 CONT {birthdayNote}"); } if (personDirectory.PersonHour is not null) { if (personDirectory.PersonHour.Status == 'D') lines.Add("1 DEAT Y"); } if (personDirectory.Notes is not null) { if (personDirectory.Notes.Length > 0) { lines.Add($"1 NOTE {personDirectory.Notes.First()}"); for (int j = 1; j < personDirectory.Notes.Length; j++) lines.Add($"2 CONT {personDirectory.Notes[j]}"); } } results.Add(id, lines.ToArray()); } if (copyOfPersonKeyToId.Count > 0) throw new Exception(string.Join(Environment.NewLine, copyOfPersonKeyToId.Keys.Select(l => new DateTime(l).ToString(appSettings.PersonBirthdayFormat)))); return new(results); } private static void WriteGenealogicalDataCommunicationCollections(Input input, GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections, ReadOnlyDictionary individualsLines) { if (input.GenealogicalDataCommunicationDirectory is not null) { List lines = new(); List allLines = new(); allLines.AddRange(genealogicalDataCommunicationCollections.HeaderLines); if (genealogicalDataCommunicationCollections.HeaderLines.Length > 0) File.WriteAllLines(Path.Combine(input.GenealogicalDataCommunicationDirectory, "a.pged"), genealogicalDataCommunicationCollections.HeaderLines); if (individualsLines.Count > 0) { foreach (IEnumerable keyValuePair in from l in individualsLines orderby l.Key select l.Value) allLines.AddRange(keyValuePair); } if (genealogicalDataCommunicationCollections.IndividualsLines.Count > 0) { lines.Clear(); foreach (KeyValuePair keyValuePair in genealogicalDataCommunicationCollections.IndividualsLines) lines.AddRange(keyValuePair.Value); if (individualsLines.Count == 0) allLines.AddRange(lines); File.WriteAllLines(Path.Combine(input.GenealogicalDataCommunicationDirectory, "b.pged"), lines); } if (genealogicalDataCommunicationCollections.FamilyGroupLines.Count > 0) { lines.Clear(); foreach (string[] keyValuePair in genealogicalDataCommunicationCollections.FamilyGroupLines) lines.AddRange(keyValuePair); allLines.AddRange(lines); File.WriteAllLines(Path.Combine(input.GenealogicalDataCommunicationDirectory, "c.pged"), lines); } allLines.AddRange(genealogicalDataCommunicationCollections.FooterLines); if (genealogicalDataCommunicationCollections.FooterLines.Length > 0) File.WriteAllLines(Path.Combine(input.GenealogicalDataCommunicationDirectory, "d.pged"), genealogicalDataCommunicationCollections.FooterLines); File.WriteAllLines(Path.Combine(input.GenealogicalDataCommunicationDirectory, "e.ged"), allLines); } } private static string? GetYearGroup(string year) => !int.TryParse(year[2..], out int part) ? null : string.Concat(year[..^2], part < 50 ? "--" : "++"); private static void Export(AppSettings appSettings, Input input, long ticks, GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections, ReadOnlyCollection personDirectories, ReadOnlyDictionary individualsLines) { if (input.Destination is not null && personDirectories.Count > 0) { int id; string[]? lines; string directory; string hourGroup; string? yearGroup; long count = ticks; string personDisplayDirectoryName; foreach (PersonDirectory personDirectory in personDirectories) { if (personDirectory.Birthday is null || personDirectory.PersonName is null) continue; if (personDirectory.DirectoryNames.Length < 3) continue; personDisplayDirectoryName = personDirectory.DirectoryNames[^2]; hourGroup = GetHourGroup(personDisplayDirectoryName, personDirectory.Birthday.Value.Hour); for (int i = 1; i < 3; i++) { if (i == 2) yearGroup = GetYearGroup(personDirectory.Birthday.Value.Year.ToString()); else if (i == 1) yearGroup = appSettings.PersonCharacters.Where(l => personDisplayDirectoryName.Contains(l)).First().ToString(); else throw new NotSupportedException(); if (string.IsNullOrEmpty(yearGroup)) continue; directory = Path.Combine(input.Destination, yearGroup, hourGroup, personDisplayDirectoryName, personDirectory.Birthday.Value.ToString(appSettings.PersonBirthdayFormat)); if (!Directory.Exists(directory)) _ = Directory.CreateDirectory(directory); if (i == 2) { if (!genealogicalDataCommunicationCollections.IndividualsLines.TryGetValue(personDirectory.Birthday.Value.Ticks, out lines)) lines = Array.Empty(); count += 1; File.WriteAllLines(Path.Combine(directory, $"{count}-A.pged"), lines); } if (!genealogicalDataCommunicationCollections.PersonKeyToId.TryGetValue(personDirectory.Birthday.Value.Ticks, out id)) lines = null; else { if (!individualsLines.TryGetValue(id, out lines)) lines = null; } if (i == 1 && lines is null) lines = Array.Empty(); if (lines is not null) { count += 1; if (i == 1) File.WriteAllLines(Path.Combine(directory, $"B.pged"), lines); else File.WriteAllLines(Path.Combine(directory, $"{count}-B.pged"), lines); } } } } } internal static void FileSystemToGenealogicalDataCommunication(AppSettings appSettings, ILogger logger, List args) { Input input = GetInput(args); long ticks = DateTime.Now.Ticks; logger.LogInformation("{ticks}", ticks); logger.LogInformation("{newValue}", new DateTime(638258293638438812).AddDays(4).Ticks); GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections = GetGenealogicalDataCommunicationCollections(appSettings, input); ReadOnlyCollection personDirectories = GetPersonDirectories(appSettings, input); ReadOnlyDictionary individualsLines = GetIndividualsLines(appSettings, genealogicalDataCommunicationCollections, personDirectories); if (string.IsNullOrEmpty(input.GenealogicalDataCommunicationDirectory)) logger.LogInformation("{distance} is null?", input.GenealogicalDataCommunicationDirectory); else if (individualsLines.Count == 0) logger.LogInformation("{count} lines?", individualsLines.Count); if (input.GenealogicalDataCommunicationDirectory is not null) WriteGenealogicalDataCommunicationCollections(input, genealogicalDataCommunicationCollections, individualsLines); if (input.Destination is not null) Export(appSettings, input, ticks, genealogicalDataCommunicationCollections, personDirectories, individualsLines); } }