diff --git a/osu.Game.Rulesets.Karaoke.Tests/Utils/TextTagsUtilsTest.cs b/osu.Game.Rulesets.Karaoke.Tests/Utils/TextTagsUtilsTest.cs new file mode 100644 index 000000000..576c5a556 --- /dev/null +++ b/osu.Game.Rulesets.Karaoke.Tests/Utils/TextTagsUtilsTest.cs @@ -0,0 +1,142 @@ +// Copyright (c) andy840119 . Licensed under the GPL Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Internal; +using NUnit.Framework; +using osu.Game.Rulesets.Karaoke.Objects; +using osu.Game.Rulesets.Karaoke.Utils; +using System; +using System.Linq; + +namespace osu.Game.Rulesets.Karaoke.Tests.Utils +{ + [TestFixture] + public class TextTagsUtilsTest + { + private const string lyric = "Test lyric"; + + [TestCase(nameof(ValidTextTagWithSorted), TextTagsUtils.Sorting.Asc, new int[] { 0, 1, 1, 2, 2, 3 })] + [TestCase(nameof(ValidTextTagWithSorted), TextTagsUtils.Sorting.Desc, new int[] { 2, 3, 1, 2, 0, 1 })] + [TestCase(nameof(ValidTextTagWithUnsorted), TextTagsUtils.Sorting.Asc, new int[] { 0, 1, 1, 2, 2, 3 })] + [TestCase(nameof(ValidTextTagWithUnsorted), TextTagsUtils.Sorting.Desc, new int[] { 2, 3, 1, 2, 0, 1 })] + public void TestSort(string testCase, TextTagsUtils.Sorting sorting, int[] results) + { + var textTags = getValueByMethodName(testCase); + + var sortedTextTags = TextTagsUtils.Sort(textTags, sorting); + for (int i = 0; i < sortedTextTags.Length; i++) + { + // result would be start, end, start, end... + Assert.AreEqual(sortedTextTags[i].StartIndex, results[i * 2]); + Assert.AreEqual(sortedTextTags[i].EndIndex, results[i * 2 + 1]); + } + } + + [TestCase(nameof(ValidTextTagWithSorted), TextTagsUtils.Sorting.Asc, new int[] { })] + [TestCase(nameof(ValidTextTagWithUnsorted), TextTagsUtils.Sorting.Asc, new int[] { })] + [TestCase(nameof(InvalidTextTagWithSameStartAndEndIndex), TextTagsUtils.Sorting.Asc, new int[] {0 })] + [TestCase(nameof(InvalidTextTagWithWrongIndex), TextTagsUtils.Sorting.Asc, new int[] { 0 })] + [TestCase(nameof(InvalidTextTagWithNegativeIndex), TextTagsUtils.Sorting.Asc, new int[] { 0 })] + [TestCase(nameof(InvalidTextTagWithEndLargerThenNextStart), TextTagsUtils.Sorting.Asc, new int[] { 1 })] + [TestCase(nameof(InvalidTextTagWithEndLargerThenNextStart), TextTagsUtils.Sorting.Desc, new int[] { 0 })] + [TestCase(nameof(InvalidTextTagWithWrapNextTextTag), TextTagsUtils.Sorting.Asc, new int[] { 1 })] + [TestCase(nameof(InvalidTextTagWithWrapNextTextTag), TextTagsUtils.Sorting.Desc, new int[] { 1 })] + [TestCase(nameof(InvalidTextTagWithSandwichTextTag), TextTagsUtils.Sorting.Asc, new int[] { 1 })] + [TestCase(nameof(InvalidTextTagWithSandwichTextTag), TextTagsUtils.Sorting.Desc, new int[] { 1 })] + public void TestFindInvalid(string testCase, TextTagsUtils.Sorting sorting, int[] errorIndex) + { + var textTags = getValueByMethodName(testCase); + + // run all and find invalid indexes. + var invalidTextTag = TextTagsUtils.FindInvalid(textTags, lyric, sorting); + var invalidIndexes = invalidTextTag.Select(v => textTags.IndexOf(v)).ToArray(); + Assert.AreEqual(invalidIndexes, errorIndex); + } + + private RubyTag[] getValueByMethodName(string methodName) + { + Type thisType = GetType(); + var theMethod = thisType.GetMethod(methodName); + if (theMethod == null) + throw new MissingMethodException("Test method is not exist."); + + return theMethod.Invoke(this, null) as RubyTag[]; + } + + #region valid source + + public static RubyTag[] ValidTextTagWithSorted() + => new[] + { + new RubyTag { StartIndex = 0, EndIndex = 1 }, + new RubyTag { StartIndex = 1, EndIndex = 2 }, + new RubyTag { StartIndex = 2, EndIndex = 3 } + }; + + public static RubyTag[] ValidTextTagWithUnsorted() + => new[] + { + new RubyTag { StartIndex = 0, EndIndex = 1 }, + new RubyTag { StartIndex = 2, EndIndex = 3 }, + new RubyTag { StartIndex = 1, EndIndex = 2 } + }; + + #endregion + + #region invalid source + + public static RubyTag[] InvalidTextTagWithWrongIndex() + => new[] + { + new RubyTag { StartIndex = 1, EndIndex = 0 }, + }; + + public static RubyTag[] InvalidTextTagWithNegativeIndex() + => new[] + { + new RubyTag { StartIndex = -1, EndIndex = 0 }, + }; + + public static RubyTag[] InvalidTextTagWithSameStartAndEndIndex() + => new[] + { + new RubyTag { StartIndex = 0, EndIndex = 0 }, // Same number. + }; + + public static RubyTag[] InvalidTextTagWithStartTimeExceedLyricSize() + => new[] + { + new RubyTag { StartIndex = 0, EndIndex = lyric.Length + 1 }, // Same number. + }; + + public static RubyTag[] InvalidTextTagWithEndTimeExceedLyricSize() + => new[] + { + new RubyTag { StartIndex = lyric.Length + 1, EndIndex = lyric.Length + 2 }, // Same number. + }; + + public static RubyTag[] InvalidTextTagWithEndLargerThenNextStart() + => new[] + { + new RubyTag { StartIndex = 0, EndIndex = 2 }, // End is larger than second start. + new RubyTag { StartIndex = 1, EndIndex = 3 } + }; + + public static RubyTag[] InvalidTextTagWithWrapNextTextTag() + => new[] + { + new RubyTag { StartIndex = 0, EndIndex = 3 }, // Wrap second text tag. + new RubyTag { StartIndex = 1, EndIndex = 2 } + }; + + public static RubyTag[] InvalidTextTagWithSandwichTextTag() + => new[] + { + new RubyTag { StartIndex = 0, EndIndex = 2 }, + new RubyTag { StartIndex = 1, EndIndex = 3 }, + new RubyTag { StartIndex = 2, EndIndex = 4 } + }; + + #endregion + } +} diff --git a/osu.Game.Rulesets.Karaoke/Edit/RubyRomaji/Components/TagListPreview.cs b/osu.Game.Rulesets.Karaoke/Edit/RubyRomaji/Components/TagListPreview.cs index 73b74223f..17e4b70c7 100644 --- a/osu.Game.Rulesets.Karaoke/Edit/RubyRomaji/Components/TagListPreview.cs +++ b/osu.Game.Rulesets.Karaoke/Edit/RubyRomaji/Components/TagListPreview.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Karaoke.Edit.RubyRomaji.Components { - public class TagListPreview : Container where T : ITag + public class TagListPreview : Container where T : ITextTag { private readonly CornerBackground background; private readonly PreviewTagTable previewTagTable; @@ -148,7 +148,7 @@ private TableColumn[] createHeaders() private Drawable[] createContent(int index, T tag) { // IDK why but it only works with Bindable, Bindable doesn't work - var bindableTag = new Bindable(tag); + var bindableTag = new Bindable(tag); OsuDropdown startPositionDropdown; OsuDropdown endPositionDropdown; diff --git a/osu.Game.Rulesets.Karaoke/Objects/RomajiTag.cs b/osu.Game.Rulesets.Karaoke/Objects/RomajiTag.cs index 44be134a4..6f4c5572b 100644 --- a/osu.Game.Rulesets.Karaoke/Objects/RomajiTag.cs +++ b/osu.Game.Rulesets.Karaoke/Objects/RomajiTag.cs @@ -5,7 +5,7 @@ namespace osu.Game.Rulesets.Karaoke.Objects { - public struct RomajiTag : ITag + public struct RomajiTag : ITextTag { /// /// If kanji Matched, then apply romaji diff --git a/osu.Game.Rulesets.Karaoke/Objects/RubyTag.cs b/osu.Game.Rulesets.Karaoke/Objects/RubyTag.cs index c13fa0d04..47a3600ab 100644 --- a/osu.Game.Rulesets.Karaoke/Objects/RubyTag.cs +++ b/osu.Game.Rulesets.Karaoke/Objects/RubyTag.cs @@ -5,7 +5,7 @@ namespace osu.Game.Rulesets.Karaoke.Objects { - public struct RubyTag : ITag + public struct RubyTag : ITextTag { /// /// If kanji Matched, then apply ruby diff --git a/osu.Game.Rulesets.Karaoke/Objects/Types/ITag.cs b/osu.Game.Rulesets.Karaoke/Objects/Types/ITextTag.cs similarity index 89% rename from osu.Game.Rulesets.Karaoke/Objects/Types/ITag.cs rename to osu.Game.Rulesets.Karaoke/Objects/Types/ITextTag.cs index 2e43ee6cb..2a1b8fc15 100644 --- a/osu.Game.Rulesets.Karaoke/Objects/Types/ITag.cs +++ b/osu.Game.Rulesets.Karaoke/Objects/Types/ITextTag.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Karaoke.Objects.Types { - public interface ITag + public interface ITextTag : IHasText { string Text { get; set; } diff --git a/osu.Game.Rulesets.Karaoke/Utils/TextTagsUtils.cs b/osu.Game.Rulesets.Karaoke/Utils/TextTagsUtils.cs new file mode 100644 index 000000000..5f5382f4d --- /dev/null +++ b/osu.Game.Rulesets.Karaoke/Utils/TextTagsUtils.cs @@ -0,0 +1,80 @@ +// Copyright (c) andy840119 . Licensed under the GPL Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Karaoke.Objects.Types; +using System.Collections.Generic; +using System.Linq; +using System; + +namespace osu.Game.Rulesets.Karaoke.Utils +{ + public static class TextTagsUtils + { + public static T[] Sort(T[] textTags, Sorting sorting = Sorting.Asc) where T : ITextTag + { + switch (sorting) + { + case Sorting.Asc: + return textTags?.OrderBy(x => x.StartIndex).ThenBy(x => x.EndIndex).ToArray(); + case Sorting.Desc: + return textTags?.OrderByDescending(x => x.EndIndex).ThenByDescending(x => x.StartIndex).ToArray(); + default: + throw new ArgumentOutOfRangeException(nameof(sorting)); + } + } + + public static T[] FindInvalid(T[] textTags, string lyric, Sorting sorting = Sorting.Asc) where T : ITextTag + { + // check is null or empty + if (textTags == null || textTags.Length == 0) + return new T[] { }; + + // todo : need to make suure is need to sort in here? + var sortedTextTags = Sort(textTags, sorting); + + var invalidList = new List(); + + // check invalid range + invalidList.AddRange(sortedTextTags.Where(x => x.StartIndex < 0 || x.EndIndex > lyric.Length)); + + // check end is less or equal to start index + invalidList.AddRange(sortedTextTags.Where(x => x.EndIndex <= x.StartIndex)); + + // find other is smaller or bigger + foreach (var textTag in sortedTextTags) + { + if (invalidList.Contains(textTag)) + continue; + + var checkTags = sortedTextTags.Except(new[] { textTag }); + switch (sorting) + { + case Sorting.Asc: + // start index within tne target + invalidList.AddRange(checkTags.Where(x => x.StartIndex >= textTag.StartIndex && x.StartIndex < textTag.EndIndex)); + break; + + case Sorting.Desc: + // end index within tne target + invalidList.AddRange(checkTags.Where(x => x.EndIndex > textTag.StartIndex && x.EndIndex <= textTag.EndIndex)); + break; + } + } + + return Sort(invalidList.Distinct().ToArray()); + } + + public enum Sorting + { + /// + /// Mark next time tag is error if conflict. + /// + Asc, + + /// + /// Mark previous tag is error if conflict. + /// + Desc + } + } +}