Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Duplicates extension #1037

Merged
merged 52 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
511bcab
introduce duplicates extension
julienasp Nov 14, 2023
6a21b2e
add local function to trigger validation before enumeration
julienasp Nov 14, 2023
ff0c3ca
getting rid of KeySelector
julienasp Nov 14, 2023
79a5e0c
add readme info
julienasp Nov 14, 2023
8db56cd
add entry in csproj
julienasp Nov 14, 2023
00259ce
update coyprights
julienasp Nov 14, 2023
ec22a9f
use null instead of default
julienasp Nov 14, 2023
b62d725
cleanup test
julienasp Nov 14, 2023
35dc3cd
add copyright region
julienasp Nov 14, 2023
277436a
place arrow on the right
julienasp Nov 15, 2023
a5f06e3
apply rename keySet
julienasp Nov 15, 2023
1742515
replace pattern matching with not operator
julienasp Nov 15, 2023
4ed65c9
add isLazy test
julienasp Nov 15, 2023
a760553
change implementation to return at most once of each duplicates
julienasp Nov 15, 2023
c57353c
re-run build.cmd
julienasp Nov 15, 2023
450cda7
fix formating
julienasp Nov 15, 2023
a19b89f
fix spaces
julienasp Nov 15, 2023
2fe88a5
add blanklines
julienasp Nov 15, 2023
3450a3f
update readme
julienasp Nov 15, 2023
c4b2b19
update doc with exception
julienasp Nov 15, 2023
551ff04
rename test cases, fix type
julienasp Nov 15, 2023
9d4c2ee
add blank after summary
julienasp Nov 15, 2023
0e564e5
update doc and regen extension
julienasp Nov 15, 2023
6c29da4
update doc
julienasp Nov 15, 2023
9536cfa
use tsource, add remarks, and rewrite doc
julienasp Nov 15, 2023
85fe597
use same return summary in doc
julienasp Nov 15, 2023
f140e75
fix warns
julienasp Nov 15, 2023
1741a49
move exception doc higher up
julienasp Nov 15, 2023
63a6f6b
try fixing warn
julienasp Nov 15, 2023
ebfac51
use testing sequence instead
julienasp Nov 15, 2023
553ed63
add empty line, ignoring CodeFactor
julienasp Nov 16, 2023
865949d
add streaming test, code from atifaziz
julienasp Nov 16, 2023
4ed9543
use scanby dry
julienasp Nov 17, 2023
83abfc5
regen build
julienasp Nov 17, 2023
bd1c304
Set package validation baseline to version 4.0
atifaziz Nov 15, 2023
22119ee
Use .NET 8 SDK to add .NET 8 target
atifaziz Nov 16, 2023
3d26c34
Update to using C# 12
atifaziz Nov 17, 2023
8401192
Address "Subsets" to-do about clearer members names
atifaziz Nov 18, 2023
c0f6c10
add .net8 unshipped info
julienasp Nov 18, 2023
4235f51
merge master and add unshipped items
julienasp Nov 18, 2023
30df553
checkout master file
julienasp Nov 18, 2023
cafb9ba
add unshipped items
julienasp Nov 18, 2023
61d74d2
Merge branch 'master' into julienasp/add-duplicates
julienasp Nov 18, 2023
3350239
apply pr suggestions
julienasp Nov 19, 2023
c932a23
change assert type
julienasp Nov 19, 2023
416bc59
change assert type
julienasp Nov 19, 2023
61b7da1
using delegate instead of custom class
julienasp Nov 19, 2023
4c1d5dc
update doc
julienasp Nov 19, 2023
7deb085
regen extensions
julienasp Nov 19, 2023
8fd8d85
Update MoreLinq/Duplicates.cs
julienasp Nov 19, 2023
ce6b842
Fix formatting
atifaziz Nov 19, 2023
5095c6f
Simplify comparer for the test
atifaziz Nov 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions MoreLinq.Test/DuplicatesTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#region License and Terms
// MoreLINQ - Extensions to LINQ to Objects
// Copyright (c) 2023 Julien Aspirot. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#endregion

namespace MoreLinq.Test
julienasp marked this conversation as resolved.
Show resolved Hide resolved
julienasp marked this conversation as resolved.
Show resolved Hide resolved
{
using System.Collections.Generic;
using NUnit.Framework;

[TestFixture]
public class DuplicatesTest
julienasp marked this conversation as resolved.
Show resolved Hide resolved
{
[Test]
public void Duplicates_IsLazy()
{
_ = new BreakingSequence<object>().Duplicates();
}

[Test]
public void Streams_Duplicates_As_They_Are_Discovered()
{
static IEnumerable<string> Source()
{
yield return "DUPLICATED_STRING";
yield return "DUPLICATED_STRING";
throw new TestException();
}

using var source = TestingSequence.Of(Source());

void Act() => source.Duplicates().Take(1);

Assert.That(Act, Throws.Nothing);
julienasp marked this conversation as resolved.
Show resolved Hide resolved
}

[Test]
public void When_Asking_For_Duplicates_On_Sequence_Without_Duplicates_Then_Empty_Sequence_Is_Returned()
julienasp marked this conversation as resolved.
Show resolved Hide resolved
{
using var testingSequence = TestingSequence.Of("FirstElement", "SecondElement", "ThirdElement");

var duplicates = testingSequence.Duplicates();
julienasp marked this conversation as resolved.
Show resolved Hide resolved

Assert.That(duplicates, Is.Empty);
}

[Test]
public void When_Asking_For_Duplicates_On_Sequence_With_Duplicates_Then_Duplicates_Are_Returned()
{
using var testingSequence = TestingSequence.Of(
"FirstElement",
"DUPLICATED_STRING",
"DUPLICATED_STRING",
"DUPLICATED_STRING",
"ThirdElement"
);
julienasp marked this conversation as resolved.
Show resolved Hide resolved

var duplicates = testingSequence.Duplicates().ToArray();

Assert.That(duplicates, Contains.Item("DUPLICATED_STRING"));
Assert.That(duplicates.AtMost(1), Is.True);
julienasp marked this conversation as resolved.
Show resolved Hide resolved
}

[Test]
public void When_Asking_For_Duplicates_On_Sequence_With_Multiple_Duplicates_Then_Duplicates_Are_Returned()
{
using var testingSequence = TestingSequence.Of(
"FirstElement",
"DUPLICATED_STRING",
"DUPLICATED_STRING",
"DUPLICATED_STRING",
"ThirdElement",
"SECOND_DUPLICATED_STRING",
"SECOND_DUPLICATED_STRING"
);
julienasp marked this conversation as resolved.
Show resolved Hide resolved

var duplicates = testingSequence.Duplicates().ToArray();
julienasp marked this conversation as resolved.
Show resolved Hide resolved

Assert.That(duplicates, Contains.Item("DUPLICATED_STRING"));
Assert.That(duplicates, Contains.Item("SECOND_DUPLICATED_STRING"));
Assert.That(duplicates.AtMost(2), Is.True);
julienasp marked this conversation as resolved.
Show resolved Hide resolved
}

[Test]
public void When_Asking_For_Duplicates_On_Sequence_With_Custom_Always_True_Comparer_Then_Duplicates_Are_Returned()
{
using var testingSequence = TestingSequence.Of("FirstElement", "SecondElement", "ThirdElement");

var duplicates = testingSequence.Duplicates(new DummyStringAlwaysTrueComparer()).ToArray();

Assert.That(duplicates.AtMost(1), Is.True);
julienasp marked this conversation as resolved.
Show resolved Hide resolved
}

[Test]
public void When_Asking_For_Duplicates_On_Sequence_With_Custom_Always_False_Comparer_Then_Empty_Sequence_Is_Returned()
{
using var testingSequence = TestingSequence.Of("FirstElement", "SecondElement", "ThirdElement");

var duplicates = testingSequence.Duplicates(new DummyStringAlwaysFalseComparer());

Assert.That(duplicates, Is.Empty);
}

[Test]
public void When_Asking_For_Duplicates_On_Multiple_Duplicates_Sequence_With_Custom_Always_False_Comparer_Then_Empty_Sequence_Is_Returned()
julienasp marked this conversation as resolved.
Show resolved Hide resolved
{
using var testingSequence = TestingSequence.Of("DUPLICATED_STRING", "DUPLICATED_STRING", "DUPLICATED_STRING");

var duplicates = testingSequence.Duplicates(new DummyStringAlwaysFalseComparer());
julienasp marked this conversation as resolved.
Show resolved Hide resolved

Assert.That(duplicates, Is.Empty);
}

sealed class DummyStringAlwaysTrueComparer : IEqualityComparer<string>
{
public bool Equals(string? x, string? y) => true;

public int GetHashCode(string obj) => 0;
}

sealed class DummyStringAlwaysFalseComparer : IEqualityComparer<string>
{
public bool Equals(string? x, string? y) => false;

public int GetHashCode(string obj) => 0;
}
}
}
64 changes: 64 additions & 0 deletions MoreLinq/Duplicates.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#region License and Terms
// MoreLINQ - Extensions to LINQ to Objects
// Copyright (c) 2023 Julien Aspirot. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#endregion

namespace MoreLinq
julienasp marked this conversation as resolved.
Show resolved Hide resolved
julienasp marked this conversation as resolved.
Show resolved Hide resolved
{
using System;
using System.Collections.Generic;
using System.Linq;

static partial class MoreEnumerable
{
/// <summary>
/// Returns all duplicate elements of the given source.
/// </summary>
/// <param name="source">The source sequence.</param>
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
julienasp marked this conversation as resolved.
Show resolved Hide resolved
/// <returns>All elements that are duplicated.</returns>
/// <remarks>This operator uses deferred execution and streams its results.</remarks>

public static IEnumerable<TSource> Duplicates<TSource>(this IEnumerable<TSource> source) =>
Duplicates(source, null);

/// <summary>
/// Returns all duplicate elements of the given source, using the specified equality
/// comparer.
/// </summary>
/// <param name="source">The source sequence.</param>
/// <param name="comparer">
/// The equality comparer to use to determine whether one <typeparamref name="TSource"/>
/// equals another. If <see langword="null"/>, the default equality comparer for
/// <typeparamref name="TSource"/> is used.</param>
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
julienasp marked this conversation as resolved.
Show resolved Hide resolved
julienasp marked this conversation as resolved.
Show resolved Hide resolved
/// <returns>All elements that are duplicated.</returns>
/// <remarks>This operator uses deferred execution and streams its results.</remarks>

public static IEnumerable<TSource> Duplicates<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource>? comparer)
julienasp marked this conversation as resolved.
Show resolved Hide resolved
{
if (source is null) throw new ArgumentNullException(nameof(source));

return from e in source.ScanBy(static e => e,
static _ => 0,
static (count, _, _) => unchecked(Math.Min(count + 1, 3)),
comparer)
where e.Value is 2
select e.Key;
julienasp marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
35 changes: 35 additions & 0 deletions MoreLinq/Extensions.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,41 @@ public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TS

}

/// <summary><c>Duplicates</c> extension.</summary>

[GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")]
public static partial class DuplicatesExtension
{
/// <summary>
/// Returns all duplicate elements of the given source.
/// </summary>
/// <param name="source">The source sequence.</param>
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
/// <returns>All elements that are duplicated.</returns>
/// <remarks>This operator uses deferred execution and streams its results.</remarks>

public static IEnumerable<TSource> Duplicates<TSource>(this IEnumerable<TSource> source) => MoreEnumerable.Duplicates(source);

/// <summary>
/// Returns all duplicate elements of the given source, using the specified equality
/// comparer.
/// </summary>
/// <param name="source">The source sequence.</param>
/// <param name="comparer">
/// The equality comparer to use to determine whether one <typeparamref name="TSource"/>
/// equals another. If <see langword="null"/>, the default equality comparer for
/// <typeparamref name="TSource"/> is used.</param>
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
/// <returns>All elements that are duplicated.</returns>
/// <remarks>This operator uses deferred execution and streams its results.</remarks>

public static IEnumerable<TSource> Duplicates<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource>? comparer)
=> MoreEnumerable.Duplicates(source, comparer);

}

/// <summary><c>EndsWith</c> extension.</summary>

[GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")]
Expand Down
1 change: 1 addition & 0 deletions MoreLinq/MoreLinq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- CountDown
- Consume
- DistinctBy
- Duplicates
- EndsWith
- EquiZip
- Evaluate
Expand Down
5 changes: 5 additions & 0 deletions MoreLinq/PublicAPI/net6.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
MoreLinq.Extensions.DuplicatesExtension
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
5 changes: 5 additions & 0 deletions MoreLinq/PublicAPI/net8.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ MoreLinq.Extensions.CountBetweenExtension
MoreLinq.Extensions.CountByExtension
MoreLinq.Extensions.CountDownExtension
MoreLinq.Extensions.DistinctByExtension
MoreLinq.Extensions.DuplicatesExtension
MoreLinq.Extensions.EndsWithExtension
MoreLinq.Extensions.EquiZipExtension
MoreLinq.Extensions.EvaluateExtension
Expand Down Expand Up @@ -183,6 +184,8 @@ static MoreLinq.Extensions.CountByExtension.CountBy<TSource, TKey>(this System.C
static MoreLinq.Extensions.CountDownExtension.CountDown<T, TResult>(this System.Collections.Generic.IEnumerable<T>! source, int count, System.Func<T, int?, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
static MoreLinq.Extensions.DistinctByExtension.DistinctBy<TSource, TKey>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Func<TSource, TKey>! keySelector) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DistinctByExtension.DistinctBy<TSource, TKey>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Func<TSource, TKey>! keySelector, System.Collections.Generic.IEqualityComparer<TKey>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.EndsWithExtension.EndsWith<T>(this System.Collections.Generic.IEnumerable<T>! first, System.Collections.Generic.IEnumerable<T>! second) -> bool
static MoreLinq.Extensions.EndsWithExtension.EndsWith<T>(this System.Collections.Generic.IEnumerable<T>! first, System.Collections.Generic.IEnumerable<T>! second, System.Collections.Generic.IEqualityComparer<T>? comparer) -> bool
static MoreLinq.Extensions.EquiZipExtension.EquiZip<T1, T2, T3, T4, TResult>(this System.Collections.Generic.IEnumerable<T1>! first, System.Collections.Generic.IEnumerable<T2>! second, System.Collections.Generic.IEnumerable<T3>! third, System.Collections.Generic.IEnumerable<T4>! fourth, System.Func<T1, T2, T3, T4, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
Expand Down Expand Up @@ -441,6 +444,8 @@ static MoreLinq.MoreEnumerable.CountBy<TSource, TKey>(this System.Collections.Ge
static MoreLinq.MoreEnumerable.CountDown<T, TResult>(this System.Collections.Generic.IEnumerable<T>! source, int count, System.Func<T, int?, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
static MoreLinq.MoreEnumerable.DistinctBy<TSource, TKey>(System.Collections.Generic.IEnumerable<TSource>! source, System.Func<TSource, TKey>! keySelector) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.DistinctBy<TSource, TKey>(System.Collections.Generic.IEnumerable<TSource>! source, System.Func<TSource, TKey>! keySelector, System.Collections.Generic.IEqualityComparer<TKey>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.EndsWith<T>(this System.Collections.Generic.IEnumerable<T>! first, System.Collections.Generic.IEnumerable<T>! second) -> bool
static MoreLinq.MoreEnumerable.EndsWith<T>(this System.Collections.Generic.IEnumerable<T>! first, System.Collections.Generic.IEnumerable<T>! second, System.Collections.Generic.IEqualityComparer<T>? comparer) -> bool
static MoreLinq.MoreEnumerable.EquiZip<T1, T2, T3, T4, TResult>(this System.Collections.Generic.IEnumerable<T1>! first, System.Collections.Generic.IEnumerable<T2>! second, System.Collections.Generic.IEnumerable<T3>! third, System.Collections.Generic.IEnumerable<T4>! fourth, System.Func<T1, T2, T3, T4, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
Expand Down
5 changes: 5 additions & 0 deletions MoreLinq/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
MoreLinq.Extensions.DuplicatesExtension
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
5 changes: 5 additions & 0 deletions MoreLinq/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
MoreLinq.Extensions.DuplicatesExtension
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.Extensions.DuplicatesExtension.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source) -> System.Collections.Generic.IEnumerable<TSource>!
static MoreLinq.MoreEnumerable.Duplicates<TSource>(this System.Collections.Generic.IEnumerable<TSource>! source, System.Collections.Generic.IEqualityComparer<TSource>? comparer) -> System.Collections.Generic.IEnumerable<TSource>!
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ projected type.

This method has 2 overloads.

### Duplicates

Returns all duplicate elements of the given source.

This method has 2 overloads.

### EndsWith

Determines whether the end of the first sequence is equivalent to the second
Expand Down
1 change: 1 addition & 0 deletions bld/Copyright.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Portions &#169; 2015 Felipe Sateler, &#8220;sholland&#8221;.
Portions &#169; 2016 Andreas Gullberg Larsen, Leandro F. Vieira (leandromoh).
Portions &#169; 2017 Jonas Nyrup (jnyrup).
Portions &#169; 2023 Julien Aspirot (julienasp).
Portions &#169; Microsoft. All rights reserved.
</Copyright>
</PropertyGroup>
Expand Down