From 48c2492647f8dda565f2083ccadf8ad6b7586f4b Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 23 Oct 2026 10:49:04 -0400 Subject: [PATCH 1/5] Match LIFT list values ignoring surrounding whitespace LIFT import matched list items, such as semantic domains, only on exact text. Semantic domain lists from FieldWorks templates before 2017 have trailing spaces in some English names (e.g., "Necessary "), so a LIFT value of "9.4.2.3 Necessary" from another program created a duplicate domain instead of linking to the existing one. Also key possibility lookups on names and abbreviations trimmed of surrounding whitespace, and fall back to the trimmed value when looking up a trait. Co-Authored-By: Claude Opus 5.5 --- .../LexTextControlsTests/LiftMergerTests.cs | 41 +++++++++++++++++++ .../LexTextControls/LiftMergerRanges.cs | 4 +- .../LiftMergerSupportCodeAndClasses.cs | 26 +++++++----- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs index 582069e9cf..6ef6bef39f 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs @@ -4803,6 +4803,47 @@ public void ImportRangeWithNoId_DoesNotDuplicateGuids_AnthroCode() Assert.That(Cache.LangProject.AnthroListOA.PossibilitiesOS, Has.Count.EqualTo(1), "Should have merged import with ICmAnthroItem from input"); } + /// + /// Semantic domain lists from FieldWorks templates before 2017 have trailing spaces + /// in some English names (e.g., "Necessary "), which other programs trim before + /// writing LIFT. + /// + [Test] + public void ImportSemanticDomainTrait_MatchesExistingDomainWithUntrimmedName() + { + SetWritingSystems("fr"); + var en = Cache.WritingSystemFactory.GetWsFromStr("en"); + var semDomList = Cache.LangProject.SemanticDomainListOA; + var semDom = Cache.ServiceLocator.GetInstance().Create(); + semDomList.PossibilitiesOS.Add(semDom); + semDom.Abbreviation.set_String(en, "9.4.2.3"); + semDom.Name.set_String(en, "Necessary "); + var semDomCount = semDomList.ReallyReallyAllPossibilities.Count; + var liftData = new[] + { + "", + "", + "", + "", + "
baba
", + "
", + "", + "", + "", + "
", + "
" + }; + + var sOrigFile = CreateInputFile(liftData); + var logFile = TryImport(sOrigFile, 1); + File.Delete(sOrigFile); + File.Delete(logFile); + + Assert.That(semDomList.ReallyReallyAllPossibilities, Has.Count.EqualTo(semDomCount), "Should not create a new semantic domain"); + var sense = Cache.ServiceLocator.GetInstance().AllInstances().Single(); + Assert.That(sense.SemanticDomainsRC.Single(), Is.SameAs(semDom)); + } + [Test] public void ImportRangeWithExistingObject_DoesNotDuplicate_UnifiesData() { diff --git a/Src/LexText/LexTextControls/LiftMergerRanges.cs b/Src/LexText/LexTextControls/LiftMergerRanges.cs index 1b4e4e8085..b90d1ae229 100644 --- a/Src/LexText/LexTextControls/LiftMergerRanges.cs +++ b/Src/LexText/LexTextControls/LiftMergerRanges.cs @@ -344,7 +344,9 @@ private static ICmPossibility FindOrCreatePossibility(string traitValue, int ws, private static bool TryGetPossibilityMatchingTrait(string traitValue, IDictionary dict, out ICmPossibility poss) { - return dict.TryGetValue(traitValue, out poss) || dict.TryGetValue(traitValue.ToLowerInvariant(), out poss); + string trimmed = traitValue.Trim(); + return dict.TryGetValue(traitValue, out poss) || dict.TryGetValue(traitValue.ToLowerInvariant(), out poss) + || dict.TryGetValue(trimmed, out poss) || dict.TryGetValue(trimmed.ToLowerInvariant(), out poss); } private static void UpdatePossibilityWithTraitValue(int ws, ICmPossibility poss, string traitValue, diff --git a/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs b/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs index 4622ecd53d..4081df4ae8 100644 --- a/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs +++ b/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs @@ -145,12 +145,20 @@ private void InitializePossibilityMap(ILcmOwningSequence possibi private static void AddToPossibilityMap(ITsString tss, ICmPossibility poss, Dictionary dict) { if (tss.Length > 0) + AddKeysToPossibilityMap(tss.Text.Normalize(), poss, dict); + } + + /// + /// Add the key as is, lowercased, and trimmed of surrounding whitespace. Some names + /// have stray whitespace (e.g., in semantic domain lists from FieldWorks templates + /// before 2017) that other programs trim before writing LIFT. + /// + private static void AddKeysToPossibilityMap(string key, ICmPossibility poss, Dictionary dict) + { + string trimmed = key.Trim(); + foreach (string s in new[] { key, key.ToLowerInvariant(), trimmed, trimmed.ToLowerInvariant() }) { - string s = tss.Text.Normalize(); - if (!dict.ContainsKey(s)) - dict.Add(s, poss); - s = s.ToLowerInvariant(); - if (!dict.ContainsKey(s)) + if (s.Length > 0 && !dict.ContainsKey(s)) dict.Add(s, poss); } } @@ -198,12 +206,8 @@ private void EnhancePossibilityMapForWeSay(ILcmOwningSequence po { string sAbbr = tssAbbr.Text; string sName = tssName.Text; - string sKey = String.Format("{0} {1}", sAbbr, sName); - if (!dict.ContainsKey(sKey)) - dict.Add(sKey, poss); - sKey = sKey.ToLowerInvariant(); - if (!dict.ContainsKey(sKey)) - dict.Add(sKey, poss); + AddKeysToPossibilityMap(String.Format("{0} {1}", sAbbr, sName), poss, dict); + AddKeysToPossibilityMap(String.Format("{0} {1}", sAbbr.Trim(), sName.Trim()), poss, dict); } } } From b536ffca52c3ec779d15afa6e6360814ff57e6b3 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 23 Oct 2026 11:16:06 -0400 Subject: [PATCH 2/5] Prefer exact LIFT list matches over trimmed ones Storing trimmed aliases in the possibility maps let an alias claim a key before an item whose name matches exactly, and items created during import got no alias, so whitespace variants within one file still created duplicates. Keep the maps as built from the project, and only when the exact and lowercase lookups miss, scan for a key that matches once trimmed. Co-Authored-By: Claude Opus 5.5 --- .../LexTextControlsTests/LiftMergerTests.cs | 74 +++++++++++++++---- .../LexTextControls/LiftMergerRanges.cs | 20 ++++- .../LiftMergerSupportCodeAndClasses.cs | 26 +++---- 3 files changed, 88 insertions(+), 32 deletions(-) diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs index 6ef6bef39f..fc86e23d13 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs @@ -4812,36 +4812,80 @@ public void ImportRangeWithNoId_DoesNotDuplicateGuids_AnthroCode() public void ImportSemanticDomainTrait_MatchesExistingDomainWithUntrimmedName() { SetWritingSystems("fr"); + var semDom = CreateEnglishSemanticDomain("9.4.2.3", "Necessary "); + var semDomCount = Cache.LangProject.SemanticDomainListOA.ReallyReallyAllPossibilities.Count; + + var senses = ImportSenseSemanticDomainTraits("9.4.2.3 Necessary"); + + Assert.That(Cache.LangProject.SemanticDomainListOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(semDomCount), "Should not create a new semantic domain"); + Assert.That(senses.Single().SemanticDomainsRC.Single(), Is.SameAs(semDom)); + } + + [Test] + public void ImportSemanticDomainTrait_PrefersExactMatchOverTrimmedMatch() + { + SetWritingSystems("fr"); + CreateEnglishSemanticDomain("9.4.2.3", "Necessary "); + var exactSemDom = CreateEnglishSemanticDomain("9.4.2.3", "Necessary"); + + var senses = ImportSenseSemanticDomainTraits("9.4.2.3 Necessary"); + + Assert.That(senses.Single().SemanticDomainsRC.Single(), Is.SameAs(exactSemDom)); + } + + [TestCase("9.4.2.3 Necessary ", "9.4.2.3 Necessary")] + [TestCase("9.4.2.3 Necessary", "9.4.2.3 Necessary ")] + public void ImportSemanticDomainTraits_DifferingOnlyInWhitespace_CreateOneDomain(string first, string second) + { + SetWritingSystems("fr"); + var semDomCount = Cache.LangProject.SemanticDomainListOA.ReallyReallyAllPossibilities.Count; + + var senses = ImportSenseSemanticDomainTraits(first, second); + + Assert.That(Cache.LangProject.SemanticDomainListOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(semDomCount + 1)); + Assert.That(senses[1].SemanticDomainsRC.Single(), Is.SameAs(senses[0].SemanticDomainsRC.Single())); + } + + private ICmSemanticDomain CreateEnglishSemanticDomain(string abbr, string name) + { var en = Cache.WritingSystemFactory.GetWsFromStr("en"); - var semDomList = Cache.LangProject.SemanticDomainListOA; var semDom = Cache.ServiceLocator.GetInstance().Create(); - semDomList.PossibilitiesOS.Add(semDom); - semDom.Abbreviation.set_String(en, "9.4.2.3"); - semDom.Name.set_String(en, "Necessary "); - var semDomCount = semDomList.ReallyReallyAllPossibilities.Count; - var liftData = new[] + Cache.LangProject.SemanticDomainListOA.PossibilitiesOS.Add(semDom); + semDom.Abbreviation.set_String(en, abbr); + semDom.Name.set_String(en, name); + return semDom; + } + + /// + /// Import one entry with a sense for each semantic domain trait value, and return its + /// senses in order. + /// + private IList ImportSenseSemanticDomainTraits(params string[] traitValues) + { + var liftData = new List { "", "", "", "", "
baba
", - "
", - "", - "", - "", - "
", - "
" + "" }; + foreach (var traitValue in traitValues) + { + liftData.Add($""); + liftData.Add($""); + liftData.Add(""); + } + liftData.Add(""); + liftData.Add(""); var sOrigFile = CreateInputFile(liftData); var logFile = TryImport(sOrigFile, 1); File.Delete(sOrigFile); File.Delete(logFile); - Assert.That(semDomList.ReallyReallyAllPossibilities, Has.Count.EqualTo(semDomCount), "Should not create a new semantic domain"); - var sense = Cache.ServiceLocator.GetInstance().AllInstances().Single(); - Assert.That(sense.SemanticDomainsRC.Single(), Is.SameAs(semDom)); + return Cache.ServiceLocator.GetInstance().AllInstances().Single().SensesOS.ToList(); } [Test] diff --git a/Src/LexText/LexTextControls/LiftMergerRanges.cs b/Src/LexText/LexTextControls/LiftMergerRanges.cs index b90d1ae229..aba9f8d4bc 100644 --- a/Src/LexText/LexTextControls/LiftMergerRanges.cs +++ b/Src/LexText/LexTextControls/LiftMergerRanges.cs @@ -344,9 +344,25 @@ private static ICmPossibility FindOrCreatePossibility(string traitValue, int ws, private static bool TryGetPossibilityMatchingTrait(string traitValue, IDictionary dict, out ICmPossibility poss) { + if (dict.TryGetValue(traitValue, out poss) || dict.TryGetValue(traitValue.ToLowerInvariant(), out poss)) + return true; + + // Some names have stray whitespace (e.g., in semantic domain lists from FieldWorks + // templates before 2017) that other programs trim before writing LIFT. string trimmed = traitValue.Trim(); - return dict.TryGetValue(traitValue, out poss) || dict.TryGetValue(traitValue.ToLowerInvariant(), out poss) - || dict.TryGetValue(trimmed, out poss) || dict.TryGetValue(trimmed.ToLowerInvariant(), out poss); + if (trimmed.Length == 0) + return false; + string trimmedLower = trimmed.ToLowerInvariant(); + foreach (var kvp in dict) + { + string key = kvp.Key.Trim(); + if (key == trimmed || key == trimmedLower) + { + poss = kvp.Value; + return true; + } + } + return false; } private static void UpdatePossibilityWithTraitValue(int ws, ICmPossibility poss, string traitValue, diff --git a/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs b/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs index 4081df4ae8..4622ecd53d 100644 --- a/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs +++ b/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs @@ -145,20 +145,12 @@ private void InitializePossibilityMap(ILcmOwningSequence possibi private static void AddToPossibilityMap(ITsString tss, ICmPossibility poss, Dictionary dict) { if (tss.Length > 0) - AddKeysToPossibilityMap(tss.Text.Normalize(), poss, dict); - } - - /// - /// Add the key as is, lowercased, and trimmed of surrounding whitespace. Some names - /// have stray whitespace (e.g., in semantic domain lists from FieldWorks templates - /// before 2017) that other programs trim before writing LIFT. - /// - private static void AddKeysToPossibilityMap(string key, ICmPossibility poss, Dictionary dict) - { - string trimmed = key.Trim(); - foreach (string s in new[] { key, key.ToLowerInvariant(), trimmed, trimmed.ToLowerInvariant() }) { - if (s.Length > 0 && !dict.ContainsKey(s)) + string s = tss.Text.Normalize(); + if (!dict.ContainsKey(s)) + dict.Add(s, poss); + s = s.ToLowerInvariant(); + if (!dict.ContainsKey(s)) dict.Add(s, poss); } } @@ -206,8 +198,12 @@ private void EnhancePossibilityMapForWeSay(ILcmOwningSequence po { string sAbbr = tssAbbr.Text; string sName = tssName.Text; - AddKeysToPossibilityMap(String.Format("{0} {1}", sAbbr, sName), poss, dict); - AddKeysToPossibilityMap(String.Format("{0} {1}", sAbbr.Trim(), sName.Trim()), poss, dict); + string sKey = String.Format("{0} {1}", sAbbr, sName); + if (!dict.ContainsKey(sKey)) + dict.Add(sKey, poss); + sKey = sKey.ToLowerInvariant(); + if (!dict.ContainsKey(sKey)) + dict.Add(sKey, poss); } } } From a51c82a58e0af5bd55f41a87aa1337738c6d16e1 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 23 Oct 2026 11:34:42 -0400 Subject: [PATCH 3/5] Trim old entry-type values when finding entry types Resolving the old entry-type trait looked up complex form and variant types by exact or lowercase text only, so an existing variant type "Foo " was missed and a new complex form type "Foo" was created. Use the same lookup as the other LIFT trait values. Co-Authored-By: Claude Opus 5.5 --- .../LexTextControlsTests/LiftMergerTests.cs | 42 +++++++++++++++++++ .../LexTextControls/LiftMergerRanges.cs | 18 +++----- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs index fc86e23d13..8bb1b2b458 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs @@ -4846,6 +4846,48 @@ public void ImportSemanticDomainTraits_DifferingOnlyInWhitespace_CreateOneDomain Assert.That(senses[1].SemanticDomainsRC.Single(), Is.SameAs(senses[0].SemanticDomainsRC.Single())); } + [Test] + public void ImportOldEntryTypeTrait_MatchesExistingVariantTypeWithUntrimmedName() + { + SetWritingSystems("fr"); + var lexDb = Cache.LangProject.LexDbOA; + var listFactory = Cache.ServiceLocator.GetInstance(); + if (lexDb.VariantEntryTypesOA == null) + lexDb.VariantEntryTypesOA = listFactory.Create(); + if (lexDb.ComplexEntryTypesOA == null) + lexDb.ComplexEntryTypesOA = listFactory.Create(); + var variantType = Cache.ServiceLocator.GetInstance().Create(); + lexDb.VariantEntryTypesOA.PossibilitiesOS.Add(variantType); + variantType.Name.set_String(Cache.WritingSystemFactory.GetWsFromStr("en"), "Foo "); + var complexTypeCount = lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities.Count; + var liftData = new[] + { + "", + "", + "", + "
baba
", + "
", + "", + "
bebe
", + "", + "", + "
", + "
" + }; + + var sOrigFile = CreateInputFile(liftData); + var logFile = TryImport(sOrigFile, 2); + File.Delete(sOrigFile); + File.Delete(logFile); + + var variantEntry = Cache.ServiceLocator.GetInstance() + .GetObject(new Guid("5e2d8c1a-9b47-4c3e-8f6a-1d0b2e4c7a93")); + var entryRef = variantEntry.EntryRefsOS.Single(); + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtVariant)); + Assert.That(entryRef.VariantEntryTypesRS.Single(), Is.SameAs(variantType)); + Assert.That(lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(complexTypeCount), "Should not create a new complex form type"); + } + private ICmSemanticDomain CreateEnglishSemanticDomain(string abbr, string name) { var en = Cache.WritingSystemFactory.GetWsFromStr("en"); diff --git a/Src/LexText/LexTextControls/LiftMergerRanges.cs b/Src/LexText/LexTextControls/LiftMergerRanges.cs index aba9f8d4bc..dc781f2368 100644 --- a/Src/LexText/LexTextControls/LiftMergerRanges.cs +++ b/Src/LexText/LexTextControls/LiftMergerRanges.cs @@ -259,23 +259,17 @@ private void FindOrCreateLexRefType(string relationTypeName, string guid, string private ILexEntryType FindComplexFormType(string sOldEntryType) { ICmPossibility poss; - if (m_dictComplexFormType.TryGetValue(sOldEntryType, out poss) || - m_dictComplexFormType.TryGetValue(sOldEntryType.ToLowerInvariant(), out poss)) - { - return poss as ILexEntryType; - } - return null; + return TryGetPossibilityMatchingTrait(sOldEntryType, m_dictComplexFormType, out poss) + ? poss as ILexEntryType + : null; } private ILexEntryType FindVariantType(string sOldEntryType) { ICmPossibility poss; - if (m_dictVariantType.TryGetValue(sOldEntryType, out poss) || - m_dictVariantType.TryGetValue(sOldEntryType.ToLowerInvariant(), out poss)) - { - return poss as ILexEntryType; - } - return null; + return TryGetPossibilityMatchingTrait(sOldEntryType, m_dictVariantType, out poss) + ? poss as ILexEntryType + : null; } private ILexEntryType FindOrCreateComplexFormType(string sType) From 285bea5098e896c572fd6aeafd3284b7e5cdf6d6 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 23 Oct 2026 12:04:54 -0400 Subject: [PATCH 4/5] Prefer exact entry-type matches over trimmed ones Resolving an old entry-type value checked complex form types first, including a trimmed match, so a complex form type "Foo " beat a variant type that matched "Foo" exactly. Try exact matches on both lists before trimmed matches on either; complex form types still win at equal strength. Co-Authored-By: Claude Opus 5.5 --- .../LexTextControlsTests/LiftMergerTests.cs | 70 ++++++++++++++----- .../LexTextControls/LiftMergerRanges.cs | 50 ++++++++----- .../LiftMergerSupportCodeAndClasses.cs | 5 +- 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs index 8bb1b2b458..42c6a0689a 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs @@ -4851,15 +4851,57 @@ public void ImportOldEntryTypeTrait_MatchesExistingVariantTypeWithUntrimmedName( { SetWritingSystems("fr"); var lexDb = Cache.LangProject.LexDbOA; - var listFactory = Cache.ServiceLocator.GetInstance(); - if (lexDb.VariantEntryTypesOA == null) - lexDb.VariantEntryTypesOA = listFactory.Create(); - if (lexDb.ComplexEntryTypesOA == null) - lexDb.ComplexEntryTypesOA = listFactory.Create(); - var variantType = Cache.ServiceLocator.GetInstance().Create(); - lexDb.VariantEntryTypesOA.PossibilitiesOS.Add(variantType); - variantType.Name.set_String(Cache.WritingSystemFactory.GetWsFromStr("en"), "Foo "); - var complexTypeCount = lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities.Count; + var variantType = CreateEnglishEntryType(EnsureEntryTypeList(false), "Foo "); + var complexTypeCount = EnsureEntryTypeList(true).ReallyReallyAllPossibilities.Count; + + var entryRef = ImportOldEntryTypeTrait("Foo"); + + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtVariant)); + Assert.That(entryRef.VariantEntryTypesRS.Single(), Is.SameAs(variantType)); + Assert.That(lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(complexTypeCount), "Should not create a new complex form type"); + } + + [Test] + public void ImportOldEntryTypeTrait_PrefersExactVariantTypeOverTrimmedComplexFormType() + { + SetWritingSystems("fr"); + CreateEnglishEntryType(EnsureEntryTypeList(true), "Foo "); + var variantType = CreateEnglishEntryType(EnsureEntryTypeList(false), "Foo"); + + var entryRef = ImportOldEntryTypeTrait("Foo"); + + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtVariant)); + Assert.That(entryRef.VariantEntryTypesRS.Single(), Is.SameAs(variantType)); + } + + private ICmPossibilityList EnsureEntryTypeList(bool complex) + { + var lexDb = Cache.LangProject.LexDbOA; + var list = complex ? lexDb.ComplexEntryTypesOA : lexDb.VariantEntryTypesOA; + if (list != null) + return list; + list = Cache.ServiceLocator.GetInstance().Create(); + if (complex) + lexDb.ComplexEntryTypesOA = list; + else + lexDb.VariantEntryTypesOA = list; + return list; + } + + private ILexEntryType CreateEnglishEntryType(ICmPossibilityList list, string name) + { + var entryType = Cache.ServiceLocator.GetInstance().Create(); + list.PossibilitiesOS.Add(entryType); + entryType.Name.set_String(Cache.WritingSystemFactory.GetWsFromStr("en"), name); + return entryType; + } + + /// + /// Import an entry with an old entry-type trait and a component relation to another + /// entry, and return the resulting entry reference. + /// + private ILexEntryRef ImportOldEntryTypeTrait(string entryType) + { var liftData = new[] { "", @@ -4869,7 +4911,7 @@ public void ImportOldEntryTypeTrait_MatchesExistingVariantTypeWithUntrimmedName( "", "", "
bebe
", - "", + $"", "", "
", "" @@ -4880,12 +4922,8 @@ public void ImportOldEntryTypeTrait_MatchesExistingVariantTypeWithUntrimmedName( File.Delete(sOrigFile); File.Delete(logFile); - var variantEntry = Cache.ServiceLocator.GetInstance() - .GetObject(new Guid("5e2d8c1a-9b47-4c3e-8f6a-1d0b2e4c7a93")); - var entryRef = variantEntry.EntryRefsOS.Single(); - Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtVariant)); - Assert.That(entryRef.VariantEntryTypesRS.Single(), Is.SameAs(variantType)); - Assert.That(lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(complexTypeCount), "Should not create a new complex form type"); + return Cache.ServiceLocator.GetInstance() + .GetObject(new Guid("5e2d8c1a-9b47-4c3e-8f6a-1d0b2e4c7a93")).EntryRefsOS.Single(); } private ICmSemanticDomain CreateEnglishSemanticDomain(string abbr, string name) diff --git a/Src/LexText/LexTextControls/LiftMergerRanges.cs b/Src/LexText/LexTextControls/LiftMergerRanges.cs index dc781f2368..f338c29a1e 100644 --- a/Src/LexText/LexTextControls/LiftMergerRanges.cs +++ b/Src/LexText/LexTextControls/LiftMergerRanges.cs @@ -256,20 +256,24 @@ private void FindOrCreateLexRefType(string relationTypeName, string guid, string } - private ILexEntryType FindComplexFormType(string sOldEntryType) - { - ICmPossibility poss; - return TryGetPossibilityMatchingTrait(sOldEntryType, m_dictComplexFormType, out poss) - ? poss as ILexEntryType - : null; - } - - private ILexEntryType FindVariantType(string sOldEntryType) + /// + /// Find the complex form type or variant type named by an old entry-type value, setting + /// at most one of the two. An exact or lowercase match beats a trimmed one; between + /// matches of equal strength, a complex form type beats a variant type. + /// + private void FindOldEntryType(string sOldEntryType, out ILexEntryType letComplex, out ILexEntryType letVar) { + letComplex = null; + letVar = null; ICmPossibility poss; - return TryGetPossibilityMatchingTrait(sOldEntryType, m_dictVariantType, out poss) - ? poss as ILexEntryType - : null; + if (TryGetPossibilityMatchingExactly(sOldEntryType, m_dictComplexFormType, out poss)) + letComplex = poss as ILexEntryType; + else if (TryGetPossibilityMatchingExactly(sOldEntryType, m_dictVariantType, out poss)) + letVar = poss as ILexEntryType; + else if (TryGetPossibilityMatchingTrimmed(sOldEntryType, m_dictComplexFormType, out poss)) + letComplex = poss as ILexEntryType; + else if (TryGetPossibilityMatchingTrimmed(sOldEntryType, m_dictVariantType, out poss)) + letVar = poss as ILexEntryType; } private ILexEntryType FindOrCreateComplexFormType(string sType) @@ -338,11 +342,25 @@ private static ICmPossibility FindOrCreatePossibility(string traitValue, int ws, private static bool TryGetPossibilityMatchingTrait(string traitValue, IDictionary dict, out ICmPossibility poss) { - if (dict.TryGetValue(traitValue, out poss) || dict.TryGetValue(traitValue.ToLowerInvariant(), out poss)) - return true; + return TryGetPossibilityMatchingExactly(traitValue, dict, out poss) + || TryGetPossibilityMatchingTrimmed(traitValue, dict, out poss); + } - // Some names have stray whitespace (e.g., in semantic domain lists from FieldWorks - // templates before 2017) that other programs trim before writing LIFT. + private static bool TryGetPossibilityMatchingExactly(string traitValue, IDictionary dict, + out ICmPossibility poss) + { + return dict.TryGetValue(traitValue, out poss) || dict.TryGetValue(traitValue.ToLowerInvariant(), out poss); + } + + /// + /// Match a key that equals the trait value once both are trimmed of surrounding + /// whitespace. Some names have stray whitespace (e.g., in semantic domain lists from + /// FieldWorks templates before 2017) that other programs trim before writing LIFT. + /// + private static bool TryGetPossibilityMatchingTrimmed(string traitValue, IDictionary dict, + out ICmPossibility poss) + { + poss = null; string trimmed = traitValue.Trim(); if (trimmed.Length == 0) return false; diff --git a/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs b/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs index 4622ecd53d..b8bcfd6ec5 100644 --- a/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs +++ b/Src/LexText/LexTextControls/LiftMergerSupportCodeAndClasses.cs @@ -3083,10 +3083,11 @@ private int DetermineLexEntryTypes(List rgRefs, List Date: Wed, 28 Oct 2026 12:37:23 -0400 Subject: [PATCH 5/5] Prefer unpadded LIFT list keys and cover entry-type lookup When a padded trait value such as "Foo " has no untrimmed match, look up the trimmed value directly before scanning the map. A key with no surrounding whitespace then wins over other keys that only match once trimmed, whatever order they were added in. Rename TryGetPossibilityMatchingExactly to TryGetPossibilityMatchingUntrimmed, since it also matches the lowercase form. Add tests for the complex form side of old entry-type lookup, the tie between complex form and variant types, minor-entry-condition subtypes, the no-match fallbacks, case-insensitive trimmed matching and padded part-of-speech names. Group the tests by behavior, name them with "Padded" for stray whitespace, and give the semantic domain senses fixed ids. Co-Authored-By: Claude Opus 5.5 --- .../LexTextControlsTests/LiftMergerTests.cs | 192 ++++++++++++++++-- .../LexTextControls/LiftMergerRanges.cs | 17 +- 2 files changed, 187 insertions(+), 22 deletions(-) diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs index 42c6a0689a..e1e1fe60b2 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/LiftMergerTests.cs @@ -4809,7 +4809,7 @@ public void ImportRangeWithNoId_DoesNotDuplicateGuids_AnthroCode() /// writing LIFT. /// [Test] - public void ImportSemanticDomainTrait_MatchesExistingDomainWithUntrimmedName() + public void ImportSemanticDomainTrait_MatchesExistingDomainWithPaddedName() { SetWritingSystems("fr"); var semDom = CreateEnglishSemanticDomain("9.4.2.3", "Necessary "); @@ -4822,7 +4822,20 @@ public void ImportSemanticDomainTrait_MatchesExistingDomainWithUntrimmedName() } [Test] - public void ImportSemanticDomainTrait_PrefersExactMatchOverTrimmedMatch() + public void ImportSemanticDomainTrait_MatchesPaddedNameIgnoringCase() + { + SetWritingSystems("fr"); + var semDom = CreateEnglishSemanticDomain("9.4.2.3", "Necessary "); + var semDomCount = Cache.LangProject.SemanticDomainListOA.ReallyReallyAllPossibilities.Count; + + var senses = ImportSenseSemanticDomainTraits("9.4.2.3 NECESSARY"); + + Assert.That(Cache.LangProject.SemanticDomainListOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(semDomCount), "Should not create a new semantic domain"); + Assert.That(senses.Single().SemanticDomainsRC.Single(), Is.SameAs(semDom)); + } + + [Test] + public void ImportSemanticDomainTrait_PrefersUntrimmedMatchOverTrimmedMatch() { SetWritingSystems("fr"); CreateEnglishSemanticDomain("9.4.2.3", "Necessary "); @@ -4847,7 +4860,57 @@ public void ImportSemanticDomainTraits_DifferingOnlyInWhitespace_CreateOneDomain } [Test] - public void ImportOldEntryTypeTrait_MatchesExistingVariantTypeWithUntrimmedName() + public void ImportGrammaticalInfo_MatchesExistingPartOfSpeechWithPaddedName() + { + SetWritingSystems("fr"); + var posList = Cache.LangProject.PartsOfSpeechOA; + var pos = Cache.ServiceLocator.GetInstance().Create(); + posList.PossibilitiesOS.Add(pos); + pos.Name.set_String(Cache.WritingSystemFactory.GetWsFromStr("en"), "Foo "); + var posCount = posList.ReallyReallyAllPossibilities.Count; + var liftData = new[] + { + "", + "", + "", + "
baba
", + "", + "", + "", + "
", + "
" + }; + + var sOrigFile = CreateInputFile(liftData); + var logFile = TryImport(sOrigFile, 1); + File.Delete(sOrigFile); + File.Delete(logFile); + + var sense = Cache.ServiceLocator.GetInstance().AllInstances().Single().SensesOS.Single(); + Assert.That(sense.MorphoSyntaxAnalysisRA, Is.AssignableTo()); + Assert.That(((IMoStemMsa)sense.MorphoSyntaxAnalysisRA).PartOfSpeechRA, Is.SameAs(pos)); + Assert.That(posList.ReallyReallyAllPossibilities, Has.Count.EqualTo(posCount), "Should not create a new part of speech"); + } + + [Test] + public void ImportOldEntryTypeTrait_MatchesExistingComplexFormTypeWithPaddedName() + { + SetWritingSystems("fr"); + var lexDb = Cache.LangProject.LexDbOA; + var complexType = CreateEnglishEntryType(EnsureEntryTypeList(true), "Foo "); + var complexTypeCount = lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities.Count; + var variantTypeCount = EnsureEntryTypeList(false).ReallyReallyAllPossibilities.Count; + + var entryRef = ImportOldEntryTypeTrait("Foo"); + + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtComplexForm)); + Assert.That(entryRef.ComplexEntryTypesRS.Single(), Is.SameAs(complexType)); + Assert.That(lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(complexTypeCount), "Should not create a new complex form type"); + Assert.That(lexDb.VariantEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(variantTypeCount), "Should not create a new variant type"); + } + + [Test] + public void ImportOldEntryTypeTrait_MatchesExistingVariantTypeWithPaddedName() { SetWritingSystems("fr"); var lexDb = Cache.LangProject.LexDbOA; @@ -4861,8 +4924,22 @@ public void ImportOldEntryTypeTrait_MatchesExistingVariantTypeWithUntrimmedName( Assert.That(lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(complexTypeCount), "Should not create a new complex form type"); } + [TestCase("Foo")] + [TestCase("Foo ")] + public void ImportOldEntryTypeTrait_PrefersComplexFormTypeOverVariantTypeOfEqualMatch(string existingName) + { + SetWritingSystems("fr"); + var complexType = CreateEnglishEntryType(EnsureEntryTypeList(true), existingName); + CreateEnglishEntryType(EnsureEntryTypeList(false), existingName); + + var entryRef = ImportOldEntryTypeTrait("Foo"); + + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtComplexForm)); + Assert.That(entryRef.ComplexEntryTypesRS.Single(), Is.SameAs(complexType)); + } + [Test] - public void ImportOldEntryTypeTrait_PrefersExactVariantTypeOverTrimmedComplexFormType() + public void ImportOldEntryTypeTrait_PrefersUntrimmedVariantTypeOverTrimmedComplexFormType() { SetWritingSystems("fr"); CreateEnglishEntryType(EnsureEntryTypeList(true), "Foo "); @@ -4874,6 +4951,89 @@ public void ImportOldEntryTypeTrait_PrefersExactVariantTypeOverTrimmedComplexFor Assert.That(entryRef.VariantEntryTypesRS.Single(), Is.SameAs(variantType)); } + [Test] + public void ImportOldEntryTypeTrait_PaddedValue_PrefersUnpaddedType() + { + SetWritingSystems("fr"); + EnsureEntryTypeList(true); + var variantList = EnsureEntryTypeList(false); + CreateEnglishEntryType(variantList, " Foo"); + var variantType = CreateEnglishEntryType(variantList, "Foo"); + + var entryRef = ImportOldEntryTypeTrait("Foo "); + + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtVariant)); + Assert.That(entryRef.VariantEntryTypesRS.Single(), Is.SameAs(variantType)); + } + + [Test] + public void ImportOldEntryTypeTraitWithCondition_AddsNewSubtypeToPaddedVariantType() + { + SetWritingSystems("fr"); + EnsureEntryTypeList(true); + var variantType = CreateEnglishEntryType(EnsureEntryTypeList(false), "Foo "); + + var entryRef = ImportOldEntryTypeTrait("Foo", "Bar"); + + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtVariant)); + var subtype = entryRef.VariantEntryTypesRS.Single(); + Assert.That(subtype.Owner, Is.SameAs(variantType)); + Assert.That(subtype.Name.AnalysisDefaultWritingSystem.Text, Is.EqualTo("Bar")); + } + + [Test] + public void ImportOldEntryTypeTraitWithCondition_ReusesSubtypeOfPaddedVariantType() + { + SetWritingSystems("fr"); + EnsureEntryTypeList(true); + var variantType = CreateEnglishEntryType(EnsureEntryTypeList(false), "Foo "); + var subtype = Cache.ServiceLocator.GetInstance().Create(); + variantType.SubPossibilitiesOS.Add(subtype); + subtype.Name.set_String(Cache.WritingSystemFactory.GetWsFromStr("en"), "Bar"); + + var entryRef = ImportOldEntryTypeTrait("Foo", "Bar"); + + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtVariant)); + Assert.That(entryRef.VariantEntryTypesRS.Single(), Is.SameAs(subtype)); + Assert.That(variantType.SubPossibilitiesOS, Has.Count.EqualTo(1), "Should not create a new subtype"); + } + + [Test] + public void ImportOldEntryTypeTrait_NoMatch_CreatesComplexFormType() + { + SetWritingSystems("fr"); + var lexDb = Cache.LangProject.LexDbOA; + var complexTypeCount = EnsureEntryTypeList(true).ReallyReallyAllPossibilities.Count; + var variantTypeCount = EnsureEntryTypeList(false).ReallyReallyAllPossibilities.Count; + + var entryRef = ImportOldEntryTypeTrait("Bar"); + + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtComplexForm)); + Assert.That(entryRef.ComplexEntryTypesRS.Single().Name.AnalysisDefaultWritingSystem.Text, Is.EqualTo("Bar")); + Assert.That(lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(complexTypeCount + 1)); + Assert.That(lexDb.VariantEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(variantTypeCount), "Should not create a new variant type"); + } + + [Test] + public void ImportOldEntryTypeTraitWithCondition_NoMatch_CreatesVariantTypeWithSubtype() + { + SetWritingSystems("fr"); + var lexDb = Cache.LangProject.LexDbOA; + var complexTypeCount = EnsureEntryTypeList(true).ReallyReallyAllPossibilities.Count; + var variantTypeCount = EnsureEntryTypeList(false).ReallyReallyAllPossibilities.Count; + + var entryRef = ImportOldEntryTypeTrait("Bar", "Baz"); + + Assert.That(entryRef.RefType, Is.EqualTo(LexEntryRefTags.krtVariant)); + var subtype = entryRef.VariantEntryTypesRS.Single(); + Assert.That(subtype.Name.AnalysisDefaultWritingSystem.Text, Is.EqualTo("Baz")); + var variantType = (ILexEntryType)subtype.Owner; + Assert.That(variantType.Name.AnalysisDefaultWritingSystem.Text, Is.EqualTo("Bar")); + Assert.That(variantType.Owner, Is.SameAs(lexDb.VariantEntryTypesOA)); + Assert.That(lexDb.VariantEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(variantTypeCount + 2)); + Assert.That(lexDb.ComplexEntryTypesOA.ReallyReallyAllPossibilities, Has.Count.EqualTo(complexTypeCount), "Should not create a new complex form type"); + } + private ICmPossibilityList EnsureEntryTypeList(bool complex) { var lexDb = Cache.LangProject.LexDbOA; @@ -4897,12 +5057,12 @@ private ILexEntryType CreateEnglishEntryType(ICmPossibilityList list, string nam } /// - /// Import an entry with an old entry-type trait and a component relation to another - /// entry, and return the resulting entry reference. + /// Import an entry with an old entry-type trait, an optional minor-entry-condition trait, + /// and a component relation to another entry, and return the resulting entry reference. /// - private ILexEntryRef ImportOldEntryTypeTrait(string entryType) + private ILexEntryRef ImportOldEntryTypeTrait(string entryType, string condition = null) { - var liftData = new[] + var liftData = new List { "", "", @@ -4911,11 +5071,13 @@ private ILexEntryRef ImportOldEntryTypeTrait(string entryType) "", "", "
bebe
", - $"", - "", - "
", - "
" + $"" }; + if (condition != null) + liftData.Add($""); + liftData.Add(""); + liftData.Add(""); + liftData.Add(""); var sOrigFile = CreateInputFile(liftData); var logFile = TryImport(sOrigFile, 2); @@ -4951,10 +5113,10 @@ private IList ImportSenseSemanticDomainTraits(params string[] traitVa "
baba
", "" }; - foreach (var traitValue in traitValues) + for (var i = 0; i < traitValues.Length; ++i) { - liftData.Add($""); - liftData.Add($""); + liftData.Add($""); + liftData.Add($""); liftData.Add(""); } liftData.Add(""); diff --git a/Src/LexText/LexTextControls/LiftMergerRanges.cs b/Src/LexText/LexTextControls/LiftMergerRanges.cs index f338c29a1e..04084dc887 100644 --- a/Src/LexText/LexTextControls/LiftMergerRanges.cs +++ b/Src/LexText/LexTextControls/LiftMergerRanges.cs @@ -266,9 +266,9 @@ private void FindOldEntryType(string sOldEntryType, out ILexEntryType letComplex letComplex = null; letVar = null; ICmPossibility poss; - if (TryGetPossibilityMatchingExactly(sOldEntryType, m_dictComplexFormType, out poss)) + if (TryGetPossibilityMatchingUntrimmed(sOldEntryType, m_dictComplexFormType, out poss)) letComplex = poss as ILexEntryType; - else if (TryGetPossibilityMatchingExactly(sOldEntryType, m_dictVariantType, out poss)) + else if (TryGetPossibilityMatchingUntrimmed(sOldEntryType, m_dictVariantType, out poss)) letVar = poss as ILexEntryType; else if (TryGetPossibilityMatchingTrimmed(sOldEntryType, m_dictComplexFormType, out poss)) letComplex = poss as ILexEntryType; @@ -342,20 +342,21 @@ private static ICmPossibility FindOrCreatePossibility(string traitValue, int ws, private static bool TryGetPossibilityMatchingTrait(string traitValue, IDictionary dict, out ICmPossibility poss) { - return TryGetPossibilityMatchingExactly(traitValue, dict, out poss) + return TryGetPossibilityMatchingUntrimmed(traitValue, dict, out poss) || TryGetPossibilityMatchingTrimmed(traitValue, dict, out poss); } - private static bool TryGetPossibilityMatchingExactly(string traitValue, IDictionary dict, + private static bool TryGetPossibilityMatchingUntrimmed(string traitValue, IDictionary dict, out ICmPossibility poss) { return dict.TryGetValue(traitValue, out poss) || dict.TryGetValue(traitValue.ToLowerInvariant(), out poss); } /// - /// Match a key that equals the trait value once both are trimmed of surrounding - /// whitespace. Some names have stray whitespace (e.g., in semantic domain lists from - /// FieldWorks templates before 2017) that other programs trim before writing LIFT. + /// Match a key that equals the trait value, or its lowercase form, once both are trimmed + /// of surrounding whitespace; a key with no surrounding whitespace wins. Some names have + /// stray whitespace (e.g., in semantic domain lists from FieldWorks templates before + /// 2017) that other programs trim before writing LIFT. /// private static bool TryGetPossibilityMatchingTrimmed(string traitValue, IDictionary dict, out ICmPossibility poss) @@ -364,6 +365,8 @@ private static bool TryGetPossibilityMatchingTrimmed(string traitValue, IDiction string trimmed = traitValue.Trim(); if (trimmed.Length == 0) return false; + if (TryGetPossibilityMatchingUntrimmed(trimmed, dict, out poss)) + return true; string trimmedLower = trimmed.ToLowerInvariant(); foreach (var kvp in dict) {