From b1e7ff6a2d4a2e7c18dcab9f1ddae3284a1fb023 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 13 May 2026 14:28:21 +0200 Subject: [PATCH 1/3] Expose CorrectHomographNumbers publicly on ILexEntry --- .../DomainImpl/OverridesLing_Lex.cs | 14 ++++++++ src/SIL.LCModel/InterfaceAdditions.cs | 8 +++++ .../SIL.LCModel.Tests/DomainImpl/LingTests.cs | 36 +++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs b/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs index f7eab80a..de212012 100644 --- a/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs +++ b/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs @@ -2044,6 +2044,20 @@ public List CollectHomographs(string sForm, int hvoExclude) return result; } + /// See . + public bool CorrectHomographNumbers() + { + if (HomographFormKey == Strings.ksQuestions) + { + if (HomographNumber == 0) return true; + HomographNumber = 0; + return false; + } + var homographs = Services.GetInstance() + .CollectHomographs(HomographFormKey, PrimaryMorphType); + return LexDb.CorrectHomographNumbers(homographs); + } + partial void LexemeFormOASideEffects(IMoForm oldObjValue, IMoForm newObjValue) { string oldVal = oldObjValue == null ? "" : oldObjValue.Form.VernacularDefaultWritingSystem.Text; diff --git a/src/SIL.LCModel/InterfaceAdditions.cs b/src/SIL.LCModel/InterfaceAdditions.cs index e0de10d8..4a2e8a24 100644 --- a/src/SIL.LCModel/InterfaceAdditions.cs +++ b/src/SIL.LCModel/InterfaceAdditions.cs @@ -1288,6 +1288,14 @@ string HomographFormKey get; } + /// + /// Validate and, if needed, correct homograph numbers for this entry's homograph set. + /// Caller should create the unit of work. + /// Primarily wraps . + /// + /// true if homographs were already valid, false if they had to be renumbered. + bool CorrectHomographNumbers(); + /// ITsString HeadWord { diff --git a/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs b/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs index 03711dc3..8daa176f 100644 --- a/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs +++ b/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs @@ -267,6 +267,42 @@ public void HomographValidationWorks() } } + /// + /// Exercises the entry-level wrapper + /// (Algorithm behaviour is covered by HomographValidationWorks.) + /// + [Test] + public void CorrectHomographNumbers_OnEntry_RenumbersInvalidSet() + { + const string sLexForm = "entryCorrectHnTest"; + var e1 = MakeEntry(sLexForm); + var e2 = MakeEntry(sLexForm); + var e3 = MakeEntry(sLexForm); + + e1.HomographNumber = 2; + e2.HomographNumber = 2; + e3.HomographNumber = 0; + + Assert.IsFalse(e1.CorrectHomographNumbers(), "Invalid set should be renumbered."); + CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, + new[] { e1.HomographNumber, e2.HomographNumber, e3.HomographNumber }); + Assert.IsTrue(e1.CorrectHomographNumbers(), "Already-valid set: no change."); + } + + /// + /// Empty-form branch added on top of LexDb.CorrectHomographNumbers. + /// + [Test] + public void CorrectHomographNumbers_OnEntryWithEmptyForm_ForcesZero() + { + var entry = MakeEntry(""); + Assert.AreEqual(Strings.ksQuestions, entry.HomographFormKey); + entry.HomographNumber = 7; + + Assert.IsFalse(entry.CorrectHomographNumbers()); + Assert.AreEqual(0, entry.HomographNumber); + } + private ILexEntry MakeEntry(string sLexForm) { var lme = Cache.ServiceLocator.GetInstance().Create(); From b46e64547000f5240fc389b7d6ae9a0a5811634e Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 15 May 2026 11:43:36 +0200 Subject: [PATCH 2/3] Move exposed method to ILexEntryRepository --- .../DomainImpl/OverridesLing_Lex.cs | 14 -------------- .../Impl/RepositoryAdditions.cs | 19 +++++++++++++++++++ src/SIL.LCModel/InterfaceAdditions.cs | 8 -------- .../RepositoryInterfaceAdditions.cs | 9 +++++++++ .../SIL.LCModel.Tests/DomainImpl/LingTests.cs | 18 ++++++++++-------- 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs b/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs index de212012..f7eab80a 100644 --- a/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs +++ b/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs @@ -2044,20 +2044,6 @@ public List CollectHomographs(string sForm, int hvoExclude) return result; } - /// See . - public bool CorrectHomographNumbers() - { - if (HomographFormKey == Strings.ksQuestions) - { - if (HomographNumber == 0) return true; - HomographNumber = 0; - return false; - } - var homographs = Services.GetInstance() - .CollectHomographs(HomographFormKey, PrimaryMorphType); - return LexDb.CorrectHomographNumbers(homographs); - } - partial void LexemeFormOASideEffects(IMoForm oldObjValue, IMoForm newObjValue) { string oldVal = oldObjValue == null ? "" : oldObjValue.Form.VernacularDefaultWritingSystem.Text; diff --git a/src/SIL.LCModel/Infrastructure/Impl/RepositoryAdditions.cs b/src/SIL.LCModel/Infrastructure/Impl/RepositoryAdditions.cs index 11108333..5356490a 100644 --- a/src/SIL.LCModel/Infrastructure/Impl/RepositoryAdditions.cs +++ b/src/SIL.LCModel/Infrastructure/Impl/RepositoryAdditions.cs @@ -1365,6 +1365,25 @@ public List CollectHomographs(string sForm, IMoMorphType morphType) return CollectHomographs(sForm, 0, GetHomographs(sForm), morphType); } + /// + /// Validate and, if needed, correct homograph numbers for the set + /// participates in. Zeros the entry's HomographNumber if form is blank (so callers + /// don't need to special-case that). + /// Caller should create the unit of work. + /// + /// true if no change was needed, false if anything was renumbered. + public bool CorrectHomographNumbers(ILexEntry entry) + { + var form = entry.HomographFormKey; + if (form == Strings.ksQuestions) + { + if (entry.HomographNumber == 0) return true; + entry.HomographNumber = 0; + return false; + } + return LexDb.CorrectHomographNumbers(CollectHomographs(form, entry.PrimaryMorphType)); + } + /// /// Main method to collect all the homographs of the given form from the given list of entries. /// Set hvo to 0 to collect absolutely every matching homograph. diff --git a/src/SIL.LCModel/InterfaceAdditions.cs b/src/SIL.LCModel/InterfaceAdditions.cs index 4a2e8a24..e0de10d8 100644 --- a/src/SIL.LCModel/InterfaceAdditions.cs +++ b/src/SIL.LCModel/InterfaceAdditions.cs @@ -1288,14 +1288,6 @@ string HomographFormKey get; } - /// - /// Validate and, if needed, correct homograph numbers for this entry's homograph set. - /// Caller should create the unit of work. - /// Primarily wraps . - /// - /// true if homographs were already valid, false if they had to be renumbered. - bool CorrectHomographNumbers(); - /// ITsString HeadWord { diff --git a/src/SIL.LCModel/RepositoryInterfaceAdditions.cs b/src/SIL.LCModel/RepositoryInterfaceAdditions.cs index 851695c3..a6b7cf8c 100644 --- a/src/SIL.LCModel/RepositoryInterfaceAdditions.cs +++ b/src/SIL.LCModel/RepositoryInterfaceAdditions.cs @@ -426,6 +426,15 @@ public partial interface ILexEntryRepository /// List CollectHomographs(string sForm, IMoMorphType morphType); + /// + /// Validate and, if needed, correct homograph numbers for the set + /// participates in. Zeros the entry's HomographNumber if form is blank (so callers + /// don't need to special-case that). + /// Caller should create the unit of work. + /// + /// true if no change was needed, false if anything was renumbered. + bool CorrectHomographNumbers(ILexEntry entry); + /// /// Maps the specified morph type onto a canonical ordering that should be used in comparing two /// entries to see whether they are homographs. diff --git a/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs b/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs index 8daa176f..641f7b5f 100644 --- a/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs +++ b/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs @@ -268,13 +268,13 @@ public void HomographValidationWorks() } /// - /// Exercises the entry-level wrapper - /// (Algorithm behaviour is covered by HomographValidationWorks.) + /// Algorithm behaviour is covered by HomographValidationWorks. + /// This just exercises the wrapper. /// [Test] - public void CorrectHomographNumbers_OnEntry_RenumbersInvalidSet() + public void CorrectHomographNumbers_RenumbersInvalidSet() { - const string sLexForm = "entryCorrectHnTest"; + const string sLexForm = "repoCorrectHnTest"; var e1 = MakeEntry(sLexForm); var e2 = MakeEntry(sLexForm); var e3 = MakeEntry(sLexForm); @@ -283,23 +283,25 @@ public void CorrectHomographNumbers_OnEntry_RenumbersInvalidSet() e2.HomographNumber = 2; e3.HomographNumber = 0; - Assert.IsFalse(e1.CorrectHomographNumbers(), "Invalid set should be renumbered."); + var repo = Cache.ServiceLocator.GetInstance(); + Assert.IsFalse(repo.CorrectHomographNumbers(e1), "Invalid set should be renumbered."); CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, new[] { e1.HomographNumber, e2.HomographNumber, e3.HomographNumber }); - Assert.IsTrue(e1.CorrectHomographNumbers(), "Already-valid set: no change."); + Assert.IsTrue(repo.CorrectHomographNumbers(e1), "Already-valid set: no change."); } /// /// Empty-form branch added on top of LexDb.CorrectHomographNumbers. /// [Test] - public void CorrectHomographNumbers_OnEntryWithEmptyForm_ForcesZero() + public void CorrectHomographNumbers_EmptyForm_ForcesZero() { var entry = MakeEntry(""); Assert.AreEqual(Strings.ksQuestions, entry.HomographFormKey); entry.HomographNumber = 7; - Assert.IsFalse(entry.CorrectHomographNumbers()); + var repo = Cache.ServiceLocator.GetInstance(); + Assert.IsFalse(repo.CorrectHomographNumbers(entry)); Assert.AreEqual(0, entry.HomographNumber); } From 4c6b8371bf474ef0f392252cce60d897582b2f65 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 13 May 2026 14:31:32 +0200 Subject: [PATCH 3/3] Use DateCreated as homograph tie-breaker --- .../DomainImpl/OverridesLing_Lex.cs | 7 ++++- .../SIL.LCModel.Tests/DomainImpl/LingTests.cs | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs b/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs index f7eab80a..09943777 100644 --- a/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs +++ b/src/SIL.LCModel/DomainImpl/OverridesLing_Lex.cs @@ -417,8 +417,13 @@ public static bool CorrectHomographNumbers(List rgHomographs) { // Should we notify the user that we're doing this helpful renumbering for him? // We do our best to keep them in the same order. + // Tie-break on DateCreated then Guid so the result is deterministic and reasonable. int n = 1; - foreach (ILexEntry le in rgHomographs.OrderBy(h => h.HomographNumber).ToList()) + foreach (ILexEntry le in rgHomographs + .OrderBy(h => h.HomographNumber) + .ThenBy(h => h.DateCreated) + .ThenBy(h => h.Guid) + .ToList()) { if (le.HomographNumber != n) le.HomographNumber = n; diff --git a/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs b/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs index 641f7b5f..ef698902 100644 --- a/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs +++ b/tests/SIL.LCModel.Tests/DomainImpl/LingTests.cs @@ -267,6 +267,36 @@ public void HomographValidationWorks() } } + /// + /// When a full renumber is needed, ties on HomographNumber are broken by + /// DateCreated (then Guid) so that the outcome is entirely deterministic. + /// + [Test] + public void CorrectHomographNumbers_FullRenumber_TieBreaksByDateCreated() + { + const string sLexForm = "tieBreakTest"; + var e1 = MakeEntry(sLexForm); + var e2 = MakeEntry(sLexForm); + var e3 = MakeEntry(sLexForm); + + e1.HomographNumber = 5; + e2.HomographNumber = 5; + e3.HomographNumber = 5; + + // Reverse natural creation order so DateCreated differs from insertion order. + var baseTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); + e3.DateCreated = baseTime; + e2.DateCreated = baseTime.AddMinutes(1); + e1.DateCreated = baseTime.AddMinutes(2); + + var fOk = LexDb.CorrectHomographNumbers(new List { e1, e2, e3 }); + + Assert.IsFalse(fOk, "CorrectHomographNumbers had to renumber homographs"); + Assert.AreEqual(1, e3.HomographNumber, "oldest DateCreated wins HN=1"); + Assert.AreEqual(2, e2.HomographNumber); + Assert.AreEqual(3, e1.HomographNumber, "newest DateCreated gets HN=3"); + } + /// /// Algorithm behaviour is covered by HomographValidationWorks. /// This just exercises the wrapper.