Skip to content

Commit 80f1701

Browse files
Added functional Apply and Let extension methods, and tests.
1 parent 852ece7 commit 80f1701

File tree

3 files changed

+143
-40
lines changed

3 files changed

+143
-40
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2020-2025 ONIXLabs
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace OnixLabs.Core.UnitTests.Data;
16+
17+
public sealed class Mutable
18+
{
19+
public required int Value { get; set; }
20+
}

OnixLabs.Core.UnitTests/ObjectExtensionTests.cs

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,31 @@ namespace OnixLabs.Core.UnitTests;
2020

2121
public sealed class ObjectExtensionTests
2222
{
23-
[Theory(DisplayName = "IsWithinRangeInclusive should produce the expected result")]
24-
[InlineData(2, 1, 3, true)]
25-
[InlineData(1, 1, 3, true)]
26-
[InlineData(3, 1, 3, true)]
27-
[InlineData(0, 1, 3, false)]
28-
[InlineData(4, 1, 3, false)]
29-
public void IsWithinRangeInclusiveShouldProduceExpectedResult(int value, int min, int max, bool expected)
23+
[Fact(DisplayName = "Apply should produce the expected result (reference type)")]
24+
public void ApplyShouldProduceExpectedResultReferenceType()
3025
{
26+
// Given
27+
Mutable value = new() { Value = 123 };
28+
3129
// When
32-
bool actual = value.IsWithinRangeInclusive(min, max);
30+
Mutable result = value.Apply(it => it.Value = 456);
3331

3432
// Then
35-
Assert.Equal(expected, actual);
33+
Assert.Equal(456, value.Value);
34+
Assert.Same(value, result);
3635
}
3736

38-
[Theory(DisplayName = "IsWithinRangeExclusive should produce the expected result")]
39-
[InlineData(2, 1, 3, true)]
40-
[InlineData(1, 1, 3, false)]
41-
[InlineData(3, 1, 3, false)]
42-
[InlineData(0, 1, 3, false)]
43-
[InlineData(4, 1, 3, false)]
44-
public void IsWithinRangeExclusiveShouldProduceExpectedResult(int value, int min, int max, bool expected)
37+
[Fact(DisplayName = "Apply should produce the expected result (value type)")]
38+
public void ApplyShouldProduceExpectedResultValueType()
4539
{
40+
// Given
41+
int value = 123;
42+
4643
// When
47-
bool actual = value.IsWithinRangeExclusive(min, max);
44+
value = value.Apply(it => it * 2);
4845

4946
// Then
50-
Assert.Equal(expected, actual);
47+
Assert.Equal(246, value);
5148
}
5249

5350
[Fact(DisplayName = "CompareToObject should produce zero if the current IComparable<T> is equal to the specified object.")]
@@ -112,6 +109,51 @@ public void CompareToObjectShouldThrowArgumentExceptionIfSpecifiedObjectIsOfInco
112109
Assert.Equal("Object must be of type System.Int32 (Parameter 'right')", exception.Message);
113110
}
114111

112+
[Theory(DisplayName = "IsWithinRangeInclusive should produce the expected result")]
113+
[InlineData(2, 1, 3, true)]
114+
[InlineData(1, 1, 3, true)]
115+
[InlineData(3, 1, 3, true)]
116+
[InlineData(0, 1, 3, false)]
117+
[InlineData(4, 1, 3, false)]
118+
public void IsWithinRangeInclusiveShouldProduceExpectedResult(int value, int min, int max, bool expected)
119+
{
120+
// When
121+
bool actual = value.IsWithinRangeInclusive(min, max);
122+
123+
// Then
124+
Assert.Equal(expected, actual);
125+
}
126+
127+
[Theory(DisplayName = "IsWithinRangeExclusive should produce the expected result")]
128+
[InlineData(2, 1, 3, true)]
129+
[InlineData(1, 1, 3, false)]
130+
[InlineData(3, 1, 3, false)]
131+
[InlineData(0, 1, 3, false)]
132+
[InlineData(4, 1, 3, false)]
133+
public void IsWithinRangeExclusiveShouldProduceExpectedResult(int value, int min, int max, bool expected)
134+
{
135+
// When
136+
bool actual = value.IsWithinRangeExclusive(min, max);
137+
138+
// Then
139+
Assert.Equal(expected, actual);
140+
}
141+
142+
[Fact(DisplayName = "Let should produce the expected result")]
143+
public void LetShouldProduceExpectedResult()
144+
{
145+
// Given
146+
const string value = "123";
147+
148+
// When
149+
int result = value
150+
.Let(int.Parse)
151+
.Let(it => it * 2);
152+
153+
// Then
154+
Assert.Equal(246, result);
155+
}
156+
115157
[Fact(DisplayName = "ToRecordString should produce null when the object is null")]
116158
public void ToRecordStringShouldProduceNullWhenObjectIsNull()
117159
{

OnixLabs.Core/Extensions.Object.cs

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,32 +41,31 @@ public static class ObjectExtensions
4141
private const string ObjectPropertyAssignment = " = ";
4242

4343
/// <summary>
44-
/// Determines whether the current <see cref="IComparable{T}"/> value falls within range, inclusive of the specified minimum and maximum values.
44+
/// Calls the specified <see cref="Action{T}"/> with the current <paramref name="value"/>.
4545
/// </summary>
46-
/// <param name="value">The value to test.</param>
47-
/// <param name="min">The inclusive minimum value.</param>
48-
/// <param name="max">The inclusive maximum value.</param>
49-
/// <typeparam name="T">The underlying <see cref="IComparable{T}"/> type.</typeparam>
50-
/// <returns>
51-
/// Returns <see langword="true"/> if the current <see cref="IComparable{T}"/> value falls within range,
52-
/// inclusive of the specified minimum and maximum values; otherwise, <see langword="false"/>.
53-
/// </returns>
54-
public static bool IsWithinRangeInclusive<T>(this T value, T min, T max) where T : IComparable<T> =>
55-
value.CompareTo(min) is 0 or 1 && value.CompareTo(max) is 0 or -1;
46+
/// <param name="value">The value to pass to the specified <see cref="Action{T}"/>.</param>
47+
/// <param name="action">The action into which the current <paramref name="value"/> will be passed.</param>
48+
/// <typeparam name="T">The underlying type of the current value.</typeparam>
49+
/// <returns>Returns the current <paramref name="value"/> once the specified <see cref="Action{T}"/> has been executed.</returns>
50+
public static T Apply<T>(this T value, Action<T> action) where T : class
51+
{
52+
RequireNotNull(action, "Action must not be null.", nameof(action));
53+
action(value);
54+
return value;
55+
}
5656

5757
/// <summary>
58-
/// Determines whether the current <see cref="IComparable{T}"/> value falls within range, exclusive of the specified minimum and maximum values.
58+
/// Calls the specified <see cref="Func{T, TResult}"/> with the current <paramref name="value"/>.
5959
/// </summary>
60-
/// <param name="value">The value to test.</param>
61-
/// <param name="min">The exclusive minimum value.</param>
62-
/// <param name="max">The exclusive maximum value.</param>
63-
/// <typeparam name="T">The underlying <see cref="IComparable{T}"/> type.</typeparam>
64-
/// <returns>
65-
/// Returns <see langword="true"/> if the current <see cref="IComparable{T}"/> value falls within range,
66-
/// exclusive of the specified minimum and maximum values; otherwise, <see langword="false"/>.
67-
/// </returns>
68-
public static bool IsWithinRangeExclusive<T>(this T value, T min, T max) where T : IComparable<T> =>
69-
value.CompareTo(min) is 1 && value.CompareTo(max) is -1;
60+
/// <param name="value">The value to pass to the specified <see cref="Func{T, TResult}"/>.</param>
61+
/// <param name="function">The function into which the current <paramref name="value"/> will be passed.</param>
62+
/// <typeparam name="T">The underlying type of the current value.</typeparam>
63+
/// <returns>Returns the result of the specified <see cref="Func{T, TResult}"/>.</returns>
64+
public static T Apply<T>(this T value, Func<T, T> function) where T : struct
65+
{
66+
RequireNotNull(function, "Function must not be null.", nameof(function));
67+
return function(value);
68+
}
7069

7170
/// <summary>
7271
/// Compares the current <typeparamref name="T"/> instance with the specified <typeparamref name="T"/> instance.
@@ -108,6 +107,48 @@ public static int CompareToNullable<T>(this T left, T? right) where T : struct,
108107
_ => throw new ArgumentException($"Object must be of type {typeof(T).FullName}", nameof(right))
109108
};
110109

110+
/// <summary>
111+
/// Determines whether the current <see cref="IComparable{T}"/> value falls within range, inclusive of the specified minimum and maximum values.
112+
/// </summary>
113+
/// <param name="value">The value to test.</param>
114+
/// <param name="min">The inclusive minimum value.</param>
115+
/// <param name="max">The inclusive maximum value.</param>
116+
/// <typeparam name="T">The underlying <see cref="IComparable{T}"/> type.</typeparam>
117+
/// <returns>
118+
/// Returns <see langword="true"/> if the current <see cref="IComparable{T}"/> value falls within range,
119+
/// inclusive of the specified minimum and maximum values; otherwise, <see langword="false"/>.
120+
/// </returns>
121+
public static bool IsWithinRangeInclusive<T>(this T value, T min, T max) where T : IComparable<T> =>
122+
value.CompareTo(min) is 0 or 1 && value.CompareTo(max) is 0 or -1;
123+
124+
/// <summary>
125+
/// Determines whether the current <see cref="IComparable{T}"/> value falls within range, exclusive of the specified minimum and maximum values.
126+
/// </summary>
127+
/// <param name="value">The value to test.</param>
128+
/// <param name="min">The exclusive minimum value.</param>
129+
/// <param name="max">The exclusive maximum value.</param>
130+
/// <typeparam name="T">The underlying <see cref="IComparable{T}"/> type.</typeparam>
131+
/// <returns>
132+
/// Returns <see langword="true"/> if the current <see cref="IComparable{T}"/> value falls within range,
133+
/// exclusive of the specified minimum and maximum values; otherwise, <see langword="false"/>.
134+
/// </returns>
135+
public static bool IsWithinRangeExclusive<T>(this T value, T min, T max) where T : IComparable<T> =>
136+
value.CompareTo(min) is 1 && value.CompareTo(max) is -1;
137+
138+
/// <summary>
139+
/// Calls the specified <see cref="Func{T, TResult}"/> with the current <paramref name="value"/> as its argument and returns the result.
140+
/// </summary>
141+
/// <param name="value">The value to pass to the specified <see cref="Func{T, TResult}"/>.</param>
142+
/// <param name="function">The function into which the current <paramref name="value"/> will be passed.</param>
143+
/// <typeparam name="TSource">The underlying type of the current value.</typeparam>
144+
/// <typeparam name="TResult">The underlying type of the result value.</typeparam>
145+
/// <returns>Returns the result of the specified <see cref="Func{T, TResult}"/>.</returns>
146+
public static TResult Let<TSource, TResult>(this TSource value, Func<TSource, TResult> function)
147+
{
148+
RequireNotNull(function, "Function must not be null.", nameof(function));
149+
return function(value);
150+
}
151+
111152
/// <summary>
112153
/// Gets a record-like <see cref="String"/> representation of the current <see cref="Object"/> instance.
113154
/// <remarks>This method is designed specifically for record-like objects and may produce undesirable results when applied to primitive-like objects.</remarks>

0 commit comments

Comments
 (0)