using System.Text.RegularExpressions;
namespace AIStudio.Assistants.BeyondFactsLab;
///
/// Suggests the names of the other participants which a session shared with the research team of the BeyondFacts Lab
/// should not contain, from what the lab knows about the event.
///
///
/// The lab knows names from three places: the field "Other participants", the headings of the research on the
/// participants, which names each person in a heading of its own, and the names in bold in the section about the
/// participants of a confirmed work state. A name is a run of two or three words with capital letters, or a single one
/// when it stands alone; longer runs give their last two words. The rules miss names and find words which are none,
/// so the person checks the suggestions and adds their own name.
///
public static partial class BflShareNames
{
///
/// The key of the section of a work state about the other participants, in the steps "Event details" and
/// "Strategy".
///
private const string PARTICIPANTS_SECTION = "PARTICIPANTS";
///
/// The most words a suggested name has.
///
private const int MAX_NAME_WORDS = 3;
///
/// Words which start a run of capitals without belonging to a name: articles, and roles which often stand before a
/// name, e.g., "Moderatorin Anna Müller".
///
private static readonly IReadOnlySet LEADING_WORDS = new HashSet(StringComparer.OrdinalIgnoreCase)
{
"Der", "Die", "Das", "Dem", "Den", "Des", "Ein", "Eine", "Einer", "The", "A", "An",
"Moderation", "Moderator", "Moderatorin", "Gast", "Gäste", "Referent", "Referentin", "Podium", "Panel", "Keynote",
"Speaker", "Host", "Chair", "Moderated", "Mit", "With",
};
///
/// Suggests the names of the other participants.
///
/// The forms of the preparation, by their steps.
/// The work states of the preparation, by their steps.
/// The research on the participants, or null when there is none.
/// The names, each once, in the order the lab found them.
public static IReadOnlyList Suggest(IReadOnlyDictionary forms, IReadOnlyDictionary workStates, BflResearch? participantsResearch)
{
var candidates = new List();
foreach (var field in BflFieldCatalog.FIELDS.Where(field => field.NameRole is BflNameRole.PARTICIPANTS))
{
if (forms.TryGetValue(field.Step, out var form))
candidates.AddRange(ListSeparatorPattern().Split(form.Get(field.Id)).SelectMany(NamesIn));
}
if (participantsResearch is not null)
candidates.AddRange(HeadingPattern().Matches(participantsResearch.Text).SelectMany(heading => NamesIn(heading.Groups["text"].Value)));
foreach (var step in BflStep.All)
{
if (workStates.GetValueOrDefault(step)?.Confirmed?.Sections.GetValueOrDefault(PARTICIPANTS_SECTION) is { } section)
candidates.AddRange(BoldPattern().Matches(section).SelectMany(bold => NamesIn(bold.Groups["text"].Value)));
}
return Distinct(candidates);
}
///
/// Gets the names in a short text, e.g., an entry of a list or a heading.
///
/// The text.
/// The names found.
internal static IEnumerable NamesIn(string text)
{
var cleaned = MarkdownLinkPattern().Replace(text, "${label}");
cleaned = MarkupPattern().Replace(cleaned, " ");
// "Moderation: Anna Müller" and "Anna Müller – TUM" put the name and its role side by side:
foreach (var segment in SegmentSeparatorPattern().Split(cleaned))
{
var words = segment.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Select(word => word.Trim(',', ';', '!', '?', '"', '„', '“', '”', '\'')).Where(word => word.Length > 0).ToList();
if (words.Count is 1)
{
if (IsNameWord(words[0]) && !LEADING_WORDS.Contains(words[0]))
yield return words[0];
continue;
}
var run = new List();
foreach (var word in words.Append(string.Empty))
{
// A particle continues a name, e.g., "Carla von der Heide", but never begins one:
if (IsNameWord(word) || run.Count > 0 && BflAnonymizer.NAME_PARTICLES.Contains(word))
{
run.Add(word);
continue;
}
if (NameOf(run) is { } name)
yield return name;
run.Clear();
}
}
}
///
/// Gets the name a run of words with capital letters stands for, if any.
///
/// The run, which may end with particles.
/// The name, or null when the run holds none.
private static string? NameOf(List run)
{
var words = run.SkipWhile(LEADING_WORDS.Contains).ToList();
while (words.Count > 0 && BflAnonymizer.NAME_PARTICLES.Contains(words[^1]))
words.RemoveAt(words.Count - 1);
if (words.Count < 2)
return null;
// Particles do not count, so that "Carla von der Heide" stays whole:
var significant = words.Count(word => !BflAnonymizer.NAME_PARTICLES.Contains(word));
return string.Join(' ', significant > MAX_NAME_WORDS ? words.TakeLast(2) : words);
}
///
/// Tells whether a word may be part of a name: it begins with a capital letter, has at least two letters or is an
/// initial, holds nothing but letters, hyphens, and apostrophes, and is no title.
///
/// The word.
private static bool IsNameWord(string word) => NameWordPattern().IsMatch(word) && !BflAnonymizer.IsTitle(word);
///
/// Removes names found twice, and a single word which is a part of a longer name found as well.
///
/// The names, in the order found.
private static IReadOnlyList Distinct(IReadOnlyList candidates)
{
var parts = candidates.Where(candidate => candidate.Contains(' ')).SelectMany(candidate => candidate.Split(' ')).ToHashSet(StringComparer.OrdinalIgnoreCase);
return [.. candidates.Where(candidate => candidate.Contains(' ') || !parts.Contains(candidate)).Distinct(StringComparer.OrdinalIgnoreCase)];
}
///
/// What separates the entries of a list of participants: lines, semicolons, commas, slashes, and "and".
///
[GeneratedRegex(@"\r?\n|;|,|/|\s+(?:und|and|sowie|&)\s+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex ListSeparatorPattern();
///
/// What separates a name from its role within an entry: a colon, a dash between spaces, or a remark in parentheses.
///
[GeneratedRegex(@":|\s[-–—]\s|\([^)]*\)|\(|\)", RegexOptions.CultureInvariant)]
private static partial Regex SegmentSeparatorPattern();
///
/// A heading of the third level, under which the research on the participants describes one person.
///
[GeneratedRegex(@"^[ \t]{0,3}###[ \t]+(?[^\n]+?)[ \t#]*$", RegexOptions.Multiline | RegexOptions.CultureInvariant)]
private static partial Regex HeadingPattern();
///
/// A text in bold.
///
[GeneratedRegex(@"\*\*(?[^*\n]+)\*\*|__(?[^_\n]+)__", RegexOptions.CultureInvariant)]
private static partial Regex BoldPattern();
///
/// A link in Markdown, whose text stays.
///
[GeneratedRegex(@"\[(?