using System.Collections.ObjectModel; using System.Globalization; namespace View_by_Distance.Shared.Models.Stateless.Methods; internal abstract class GenealogicalDataCommunication { // ... 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 List GetRelations(Dictionary idToNick, List familyGroupLines) { List results = new(); string? nick; string relation; string[] segments; string[] familyLines; for (int i = 0; i < familyGroupLines.Count; i++) { familyLines = familyGroupLines[i]; for (int j = 0; j < familyLines.Length; j++) { segments = familyLines[j].Split('@'); if (segments.First().Length < 3 || segments.Length != 3) continue; if (!idToNick.TryGetValue(segments[1], out nick)) continue; relation = segments.First()[2..].Trim(); if (j + 1 >= familyLines.Length || familyLines[j + 1].Length < 3 || familyLines[j + 1][..3] != "2 _") results.Add(new(i, relation, segments[1], nick, null)); else results.Add(new(i, relation, segments[1], nick, familyLines[j + 1][2..])); } } return results; } private static ReadOnlyDictionary Convert(Dictionary> keyValuePairs) { Dictionary results = new(); foreach (KeyValuePair> keyValuePair in keyValuePairs) results.Add(keyValuePair.Key, keyValuePair.Value.ToArray()); return new(results); } internal static (string[], ReadOnlyDictionary, List, string[], List genealogicalDataCommunicationRelations) GetIndividuals(string genealogicalDataCommunicationFile, bool requireNickName) { ReadOnlyDictionary results; string? nick; string[] segments; List lines = new(); const string startsWith = "0 @"; List footerLines = new(); List familyGroupLines = new(); Dictionary idToNick = new(); Dictionary> keyValuePairs = new(); string[] sourceLines = string.IsNullOrEmpty(genealogicalDataCommunicationFile) || !File.Exists(genealogicalDataCommunicationFile) ? Array.Empty() : File.ReadAllLines(genealogicalDataCommunicationFile); string[] headerLines = GetHeaderLines(startsWith, sourceLines); for (int i = headerLines.Length; i < sourceLines.Length; i++) { if (!sourceLines[i].StartsWith(startsWith)) continue; nick = null; 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")) { 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 ")) continue; nick = sourceLines[j][7..]; if (segments.Length == 3) idToNick.Add(segments[1], nick); } if (requireNickName && string.IsNullOrEmpty(nick)) throw new Exception(string.Join(Environment.NewLine, lines)); nick ??= Guid.NewGuid().ToString(); keyValuePairs.Add(nick, new()); if (!lines.Any()) continue; keyValuePairs[nick].AddRange(lines); lines.Clear(); } else throw new NotSupportedException(); } results = Convert(keyValuePairs); List genealogicalDataCommunicationRelations = GetRelations(idToNick, familyGroupLines); return (headerLines, results, familyGroupLines, footerLines.ToArray(), genealogicalDataCommunicationRelations); } private static string[] GetFilteredOutMapped(string[] individualsLines) { List results = new(); for (int i = 0; i < individualsLines.Length; i++) { if (individualsLines[i].StartsWith("0 @I")) continue; else if (individualsLines[i].StartsWith("1 _UID")) continue; else if (individualsLines[i].StartsWith("1 NAME")) continue; else if (individualsLines[i].StartsWith("2 GIVN")) continue; else if (individualsLines[i].StartsWith("2 SURN")) continue; else if (individualsLines[i].StartsWith("2 NSFX")) continue; else if (individualsLines[i].StartsWith("2 NICK")) continue; else if (individualsLines[i].StartsWith("1 SEX")) continue; else if (individualsLines[i] == "1 BIRT") { if (individualsLines.Length > i + 1 && individualsLines[i + 1].StartsWith("2 DATE")) i += 1; continue; } else if (individualsLines[i].StartsWith("1 DEAT")) { if (individualsLines.Length > i + 1 && individualsLines[i + 1].StartsWith("2 DATE")) i += 1; continue; } else if (individualsLines[i] == "1 CHAN") { if (individualsLines.Length > i + 1 && individualsLines[i + 1].StartsWith("2 DATE")) i += 1; continue; } results.Add(individualsLines[i]); } return results.ToArray(); } internal static List GetMappedLines(string genealogicalDataCommunicationFile, bool requireNickName) { List results = new(); Models.PersonBirthday personBirthday = new(DateTime.Now); GenealogicalDataCommunicationLines genealogicalDataCommunicationLines; (string[] headerLines, ReadOnlyDictionary individuals, List familyGroupLines, string[] footerLines, _) = GetIndividuals(genealogicalDataCommunicationFile, requireNickName); results.AddRange(headerLines); foreach (KeyValuePair keyValuePair in individuals) { genealogicalDataCommunicationLines = GetGenealogicalDataCommunicationLines(personBirthday, keyValuePair.Value); if (!string.IsNullOrEmpty(genealogicalDataCommunicationLines.Id)) results.Add(genealogicalDataCommunicationLines.Id); if (!string.IsNullOrEmpty(genealogicalDataCommunicationLines.UId)) results.Add(genealogicalDataCommunicationLines.UId); if (!string.IsNullOrEmpty(genealogicalDataCommunicationLines.Name)) results.Add(genealogicalDataCommunicationLines.Name); if (!string.IsNullOrEmpty(genealogicalDataCommunicationLines.GivenName)) results.Add(genealogicalDataCommunicationLines.GivenName); if (!string.IsNullOrEmpty(genealogicalDataCommunicationLines.SurName)) results.Add(genealogicalDataCommunicationLines.SurName); if (!string.IsNullOrEmpty(genealogicalDataCommunicationLines.NickName)) results.Add(genealogicalDataCommunicationLines.NickName); if (!string.IsNullOrEmpty(genealogicalDataCommunicationLines.Sex)) results.Add(genealogicalDataCommunicationLines.Sex); results.AddRange(genealogicalDataCommunicationLines.Birth); results.AddRange(genealogicalDataCommunicationLines.Death); results.AddRange(genealogicalDataCommunicationLines.Changed); } for (int i = 0; i < familyGroupLines.Count; i++) results.AddRange(familyGroupLines[i]); results.AddRange(footerLines); return results; } internal static GenealogicalDataCommunicationLines GetGenealogicalDataCommunicationLines(Models.PersonBirthday personBirthday, string[] individualsLines) { GenealogicalDataCommunicationLines result; string? idLine = null; string? sexLine = null; string? uIdLine = null; string? nameLine = null; string? suffixLine = null; string? surNameLine = null; string? nickNameLine = null; string? givenNameLine = null; List birthLines = new(); List deathLines = new(); List changedLines = new(); for (int i = 0; i < individualsLines.Length; i++) { if (idLine is null && individualsLines[i].EndsWith("@ INDI")) idLine = individualsLines[i]; else if (uIdLine is null && individualsLines[i].StartsWith("1 _UID")) uIdLine = individualsLines[i]; else if (nameLine is null && individualsLines[i].StartsWith("1 NAME")) nameLine = individualsLines[i]; else if (givenNameLine is null && individualsLines[i].StartsWith("2 GIVN")) givenNameLine = individualsLines[i]; else if (surNameLine is null && individualsLines[i].StartsWith("2 SURN")) surNameLine = individualsLines[i]; else if (suffixLine is null && individualsLines[i].StartsWith("2 NSFX")) suffixLine = individualsLines[i]; else if (nickNameLine is null && individualsLines[i].StartsWith("2 NICK")) nickNameLine = individualsLines[i]; else if (sexLine is null && individualsLines[i].StartsWith("1 SEX")) sexLine = individualsLines[i]; else if (!birthLines.Any() && individualsLines[i] == "1 BIRT") { birthLines.Add(individualsLines[i]); if (individualsLines.Length > i + 1 && individualsLines[i + 1].StartsWith("2 DATE")) { i += 1; birthLines.Add(individualsLines[i]); } } else if (!deathLines.Any() && individualsLines[i].StartsWith("1 DEAT")) { deathLines.Add(individualsLines[i]); if (individualsLines.Length > i + 1 && individualsLines[i + 1].StartsWith("2 DATE")) { i += 1; deathLines.Add(individualsLines[i]); } } else if (!changedLines.Any() && individualsLines[i] == "1 CHAN") { changedLines.Add(individualsLines[i]); if (individualsLines.Length > i + 1 && individualsLines[i + 1].StartsWith("2 DATE")) { i += 1; changedLines.Add(individualsLines[i]); } } } if (!birthLines.Any()) { birthLines.Add("1 BIRT"); birthLines.Add($"2 DATE {personBirthday.Value:dd MMM yyyy}"); } result = new(idLine, uIdLine, nameLine, givenNameLine, surNameLine, suffixLine, nickNameLine, sexLine, birthLines, deathLines, changedLines); return result; } internal static Models.GenealogicalDataCommunication GetGenealogicalDataCommunication(GenealogicalDataCommunicationLines genealogicalDataCommunicationLines) { Models.GenealogicalDataCommunication result; DateTime? birth; DateTime? death; DateTime? changed; char sex = string.IsNullOrEmpty(genealogicalDataCommunicationLines.Sex) || !genealogicalDataCommunicationLines.Sex.Contains("1 SEX ") ? 'U' : genealogicalDataCommunicationLines.Sex.Split("1 SEX ")[1][0]; string? uId = string.IsNullOrEmpty(genealogicalDataCommunicationLines.UId) || !genealogicalDataCommunicationLines.UId.Contains("1 _UID ") ? null : genealogicalDataCommunicationLines.UId.Split("1 _UID ")[1]; string? name = string.IsNullOrEmpty(genealogicalDataCommunicationLines.Name) || !genealogicalDataCommunicationLines.Name.Contains("1 NAME ") ? null : genealogicalDataCommunicationLines.Name.Split("1 NAME ")[1]; string? suffix = string.IsNullOrEmpty(genealogicalDataCommunicationLines.Suffix) || !genealogicalDataCommunicationLines.Suffix.Contains("2 NSFX ") ? null : genealogicalDataCommunicationLines.Suffix.Split("2 NSFX ")[1]; string? surName = string.IsNullOrEmpty(genealogicalDataCommunicationLines.SurName) || !genealogicalDataCommunicationLines.SurName.Contains("2 SURN ") ? null : genealogicalDataCommunicationLines.SurName.Split("2 SURN ")[1]; string? nickName = string.IsNullOrEmpty(genealogicalDataCommunicationLines.NickName) || !genealogicalDataCommunicationLines.NickName.Contains("2 NICK ") ? null : genealogicalDataCommunicationLines.NickName.Split("2 NICK ")[1]; string? givenName = string.IsNullOrEmpty(genealogicalDataCommunicationLines.GivenName) || !genealogicalDataCommunicationLines.GivenName.Contains("2 GIVN ") ? null : genealogicalDataCommunicationLines.GivenName.Split("2 GIVN ")[1]; string? id = string.IsNullOrEmpty(genealogicalDataCommunicationLines.Id) || !genealogicalDataCommunicationLines.Id.Contains("0 @I") || !genealogicalDataCommunicationLines.Id.Contains("@ INDI") ? null : genealogicalDataCommunicationLines.Id.Split('@')[1][1..]; string[] birthLines = (from l in genealogicalDataCommunicationLines.Birth where l.Contains("2 DATE ") select l.Split("2 DATE ")[1]).ToArray(); string[] deathLines = (from l in genealogicalDataCommunicationLines.Death where l.Contains("2 DATE ") select l.Split("2 DATE ")[1]).ToArray(); string[] changedLines = (from l in genealogicalDataCommunicationLines.Changed where l.Contains("2 DATE ") select l.Split("2 DATE ")[1]).ToArray(); if (!birthLines.Any() || !DateTime.TryParse(birthLines[0], out DateTime parseBirth)) birth = null; else birth = parseBirth; if (!deathLines.Any() || !DateTime.TryParse(deathLines[0], out DateTime parseDeath)) death = null; else death = parseDeath; if (!changedLines.Any() || !DateTime.TryParse(changedLines[0], out DateTime parseChanged)) changed = null; else changed = parseChanged; if (birth is not null) { bool alive = death is null && !genealogicalDataCommunicationLines.Death.Any(l => l == "1 DEAT Y"); (int age, _) = IAge.GetAge(DateTime.Now.Ticks, birth.Value.Ticks); int hours = IPersonBirthday.GetHour(alive, sex); if (death == birth) death = death.Value.AddHours(hours); birth = birth.Value.AddHours(hours); if (age < 1) birth = null; if (birth is not null && death is null && (!alive || (age > 110 && !IPersonBirthday.IsCounterPersonBirthday(new(birth.Value))))) death = birth; } death ??= !genealogicalDataCommunicationLines.Death.Any(l => l == "1 DEAT Y") ? null : birth; result = new(id, uId, name, givenName, surName, suffix, nickName, sex, birth, death, changed); return result; } internal static void WriteFile(string personKeyFormatted, Models.PersonName personName, string[]? individualsLines, bool isDefaultName, string directory, Models.GenealogicalDataCommunication genealogicalDataCommunication, bool verify, bool first) { if (verify) { if (genealogicalDataCommunication.SurName != personName.Last.Value) throw new Exception($"{genealogicalDataCommunication.Name} - {personKeyFormatted}"); } string jrOrSr; if (string.IsNullOrEmpty(personName.Alias.Value)) jrOrSr = string.Empty; else { if (personName.Alias.Value.Contains(" Jr")) jrOrSr = " Jr"; else if (personName.Alias.Value.Contains(" Sr")) jrOrSr = " Sr"; else jrOrSr = string.Empty; } string code = IPersonBirthday.GetHour(genealogicalDataCommunication.Death is null, genealogicalDataCommunication.Sex).ToString("00"); if (directory.EndsWith("00")) directory = string.Concat(directory[..^2], code); else if (directory.EndsWith("01")) directory = string.Concat(directory[..^2], code); if (!Directory.Exists(directory)) _ = Directory.CreateDirectory(directory); if (first && !personKeyFormatted.EndsWith(code)) personKeyFormatted = $"{personKeyFormatted[..^2]}{code}"; List lines = new(); if (individualsLines is null || !individualsLines.Any()) lines.Add($"0 @I{personKeyFormatted}@ INDI"); else { if (!individualsLines[0].StartsWith("0 @I")) throw new NotSupportedException(); lines.Add(individualsLines[0]); } lines.Add($"1 NAME {personName.First.Value} /{personName.Last.Value}/{jrOrSr}"); if (!string.IsNullOrEmpty(personName.First.Value)) lines.Add($"2 GIVN {personName.First.Value}"); if (!string.IsNullOrEmpty(personName.Last.Value)) lines.Add($"2 SURN {personName.Last.Value}"); if (!string.IsNullOrEmpty(jrOrSr)) lines.Add($"2 NSFX {jrOrSr.Trim()}"); lines.Add($"2 NICK {personKeyFormatted}"); lines.Add($"1 SEX {genealogicalDataCommunication.Sex}"); if (genealogicalDataCommunication.Birth is not null) { Models.PersonBirthday personBirthday = new(genealogicalDataCommunication.Birth.Value); if (!IPersonBirthday.IsCounterPersonBirthday(personBirthday)) { lines.Add("1 BIRT"); lines.Add($"2 DATE {genealogicalDataCommunication.Birth.Value:dd MMM yyyy}"); } } if (genealogicalDataCommunication.Death is not null) { if (genealogicalDataCommunication?.Death is null || genealogicalDataCommunication.Death == genealogicalDataCommunication.Birth || IPersonBirthday.IsCounterPersonBirthday(new(genealogicalDataCommunication.Death.Value))) lines.Add("1 DEAT Y"); else { lines.Add("1 DEAT"); lines.Add($"2 DATE {genealogicalDataCommunication.Death.Value:dd MMM yyyy}"); } } if (isDefaultName) lines.Add("9 NOTE"); if (individualsLines is not null) lines.AddRange(GetFilteredOutMapped(individualsLines)); string text = string.Join(Environment.NewLine, lines); _ = IPath.WriteAllText(Path.Combine(directory, $"{personKeyFormatted}.pged"), text, updateDateWhenMatches: false, compareBeforeWrite: true); } internal static bool CleanDisplayDirectoryAllFilesAndWriteTicksGed(string mappingDefaultName, string personBirthdayFormat, List personContainers, string[] headerLines, List familyGroupLines, string[] footerLines, long ticks, string a2PeopleContentDirectory) { bool result = false; string[] mdFiles; string[] txtFiles; const int zero = 0; string[] jsonFiles; string[] pGedFiles; string personKeyFormatted; List lines = new(); List distinct = new(); List individualsLines; DateTime dateTime = new(ticks); Models.PersonBirthday personBirthday; Calendar calendar = new CultureInfo("en-US").Calendar; string weekOfYear = calendar.GetWeekOfYear(dateTime, CalendarWeekRule.FirstDay, DayOfWeek.Sunday).ToString("00"); lines.AddRange(headerLines); foreach (Models.PersonContainer personContainer in personContainers) { if (personContainer.Key is null || personContainer.Birthdays is null || personContainer.Person is null || !personContainer.Birthdays.Any()) continue; if (IPerson.IsDefaultName(mappingDefaultName, personContainer.DisplayDirectoryName) || IPerson.IsDefaultName(mappingDefaultName, personContainer.Person)) continue; if (distinct.Contains(personContainer.Key.Value)) continue; distinct.Add(personContainer.Key.Value); personBirthday = personContainer.Birthdays[zero]; personKeyFormatted = IPersonBirthday.GetFormatted(personBirthdayFormat, personBirthday); mdFiles = (from l in personContainer.DisplayDirectoryAllFiles where l.EndsWith(".md") select l).ToArray(); txtFiles = (from l in personContainer.DisplayDirectoryAllFiles where l.EndsWith(".txt") select l).ToArray(); jsonFiles = (from l in personContainer.DisplayDirectoryAllFiles where l.EndsWith(".json") select l).ToArray(); pGedFiles = (from l in personContainer.DisplayDirectoryAllFiles where l.EndsWith(".pged") select l).ToArray(); foreach (string mdFile in mdFiles) { if (string.IsNullOrEmpty(personKeyFormatted)) continue; if (!mdFile.Contains(personKeyFormatted)) { if (!result) result = true; File.Delete(mdFile); } } foreach (string pGedFile in pGedFiles) { if (string.IsNullOrEmpty(personKeyFormatted)) continue; if (!pGedFile.Contains(personKeyFormatted)) { if (!result) result = true; File.Delete(pGedFile); continue; } individualsLines = File.ReadAllLines(pGedFile).ToList(); foreach (string jsonFile in jsonFiles) { if (!result) result = true; File.Delete(jsonFile); } foreach (string txtFile in txtFiles) { if (new FileInfo(txtFile).Length == 0) { if (!result) result = true; File.Delete(txtFile); } } lines.AddRange(individualsLines); } } for (int i = 0; i < familyGroupLines.Count; i++) lines.AddRange(familyGroupLines[i]); lines.AddRange(footerLines); File.WriteAllLines(Path.Combine(a2PeopleContentDirectory, $"{dateTime.Year}-GenealogicalDataCommunication", $"{dateTime.Year}-Week-{weekOfYear}", $"{ticks}.ged"), lines); return result; } }