From 022cc139528eb581029d7dcf0a7935ef34d0350a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Oct 2019 22:55:46 +0100 Subject: [PATCH 1/3] Add beatmap carousel item sorting stability test Add visual test to ensure sorting stability when sorting criteria are applied in the beatmap carousel. --- .../SongSelect/TestSceneBeatmapCarousel.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index f87d6ebebb..8b82567a8d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -245,6 +245,28 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!")); } + [Test] + public void TestSortingStability() + { + var sets = new List(); + + for (int i = 0; i < 20; i++) + { + var set = createTestBeatmapSet(i); + set.Metadata.Artist = "same artist"; + set.Metadata.Title = "same title"; + sets.Add(set); + } + + loadBeatmaps(sets); + + AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); + AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b)); + + AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); + AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b)); + } + [Test] public void TestSortingWithFiltered() { From c8d3dd0e5a62e126eb9e2d7701aec7e731a5effb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Oct 2019 23:14:14 +0100 Subject: [PATCH 2/3] Make carousel item sorting stable Migrate beatmap carousel item sorting from List.Sort() to IEnumerable.OrderBy(), as the second variant is documented to be a stable sorting algorithm. This allows for eliminating unnecessary movement of carousel items occurring whenever any set of items is tied when changing sorting criteria. --- .../Screens/Select/Carousel/CarouselGroup.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs index 09b728abeb..b32561eb88 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Linq; namespace osu.Game.Screens.Select.Carousel { @@ -81,12 +83,9 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - var children = new List(InternalChildren); - - children.ForEach(c => c.Filter(criteria)); - children.Sort((x, y) => x.CompareTo(criteria, y)); - - InternalChildren = children; + InternalChildren.ForEach(c => c.Filter(criteria)); + // IEnumerable.OrderBy() is used instead of List.Sort() to ensure sorting stability + InternalChildren = InternalChildren.OrderBy(c => c, new CriteriaComparer(criteria)).ToList(); } protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value) @@ -104,5 +103,23 @@ namespace osu.Game.Screens.Select.Carousel State.Value = CarouselItemState.Selected; } } + + private class CriteriaComparer : IComparer + { + private readonly FilterCriteria criteria; + + public CriteriaComparer(FilterCriteria criteria) + { + this.criteria = criteria; + } + + public int Compare(CarouselItem x, CarouselItem y) + { + if (x != null && y != null) + return x.CompareTo(criteria, y); + + throw new ArgumentNullException(); + } + } } } From c181edaedfc59cd9f3cfc74f7f817059b8051dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Oct 2019 15:07:36 +0100 Subject: [PATCH 3/3] Replace manual comparer implementation Replace manually-implemented CriteriaComparer with a call to Comparer.Create() to decrease verbosity. --- .../Screens/Select/Carousel/CarouselGroup.cs | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs index b32561eb88..aa48d1a04e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; @@ -85,7 +84,8 @@ namespace osu.Game.Screens.Select.Carousel InternalChildren.ForEach(c => c.Filter(criteria)); // IEnumerable.OrderBy() is used instead of List.Sort() to ensure sorting stability - InternalChildren = InternalChildren.OrderBy(c => c, new CriteriaComparer(criteria)).ToList(); + var criteriaComparer = Comparer.Create((x, y) => x.CompareTo(criteria, y)); + InternalChildren = InternalChildren.OrderBy(c => c, criteriaComparer).ToList(); } protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value) @@ -103,23 +103,5 @@ namespace osu.Game.Screens.Select.Carousel State.Value = CarouselItemState.Selected; } } - - private class CriteriaComparer : IComparer - { - private readonly FilterCriteria criteria; - - public CriteriaComparer(FilterCriteria criteria) - { - this.criteria = criteria; - } - - public int Compare(CarouselItem x, CarouselItem y) - { - if (x != null && y != null) - return x.CompareTo(criteria, y); - - throw new ArgumentNullException(); - } - } } }