Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
71 changes: 69 additions & 2 deletions NExtends.Tests/Primitives/DateTimes/PeriodTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using NExtends.Primitives.DateTimes;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;

namespace NExtends.Tests.Primitives.DateTimes
Expand Down Expand Up @@ -31,5 +29,74 @@ public void PeriodShouldWorkOnDateTimes()
Assert.Equal(startsAt, period.Start);
Assert.Equal(endsAt, period.End);
}

[Fact]
public void PeriodCannotBeNegative()
{
Assert.Throws<NegativeDurationException>(() =>
{
var period = new Period(new DateTime(2018, 10, 30), new DateTime(2018, 10, 28));
});
}

[Fact]
public void PeriodCanBeZeroSize()
{
var startAndEnd = new DateTime(2018, 10, 30);

var period = new Period(startAndEnd, startAndEnd);
}

[Fact]
public void ITimeBlockEndCannotBeModifiedToBeNegative()
{
var startAndEnd = new DateTime(2018, 10, 30);

ITimeBlock period = new Period(startAndEnd, startAndEnd);

Assert.Throws<NegativeDurationException>(() =>
{
period.ChangeEndsAt(new DateTime(2018, 10, 28));
});
}

[Fact]
public void ITimeBlockStartCannotBeModifiedToBeNegative()
{
var startAndEnd = new DateTime(2018, 10, 28);

ITimeBlock period = new Period(startAndEnd, startAndEnd);

Assert.Throws<NegativeDurationException>(() =>
{
period.ChangeStartsAt(new DateTime(2018, 10, 30));
});
}

[Fact]
public void ITimeBlockDurationCannotBeModifiedToBeNegative()
{
var startAndEnd = new DateTime(2018, 10, 28);

ITimeBlock period = new Period(startAndEnd, startAndEnd);

Assert.Throws<NegativeDurationException>(() =>
{
period.ChangeDuration(TimeSpan.FromSeconds(-1));
});
}

[Fact]
public void ITimeBlockDurationCanBeDifferentFromEndMinusStart()
{
var startAndEnd = new DateTime(2018, 10, 28);

ITimeBlock period = new Period(startAndEnd, startAndEnd);

period.ChangeDuration(TimeSpan.FromSeconds(1));

Assert.Equal(TimeSpan.FromSeconds(1), period.Duration);
Assert.Equal(TimeSpan.FromSeconds(0), period.EndsAt - period.StartsAt);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public void ContainsIgnoreCaseShouldWork(string match, string chain)
Assert.True(result);
}

[Theory]
[InlineData("toto")]
[InlineData(null)]
[InlineData("")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using NExtends.Primitives.TimeSpans;
using System;
using System.Globalization;
using Xunit;

namespace NExtends.Tests.Primitives.TimeSpans
Expand All @@ -20,10 +21,10 @@ public void TimespanMultiply()
[Theory]
[InlineData(0, true, "-")]
[InlineData(0, false, "-")]
[InlineData(9, true, "+09mn")]
[InlineData(9, false, "09mn")]
[InlineData(-9, true, "-09mn")]
[InlineData(-9, false, "-09mn")]
[InlineData(9, true, "+09m")]
[InlineData(9, false, "09m")]
[InlineData(-9, true, "-09m")]
[InlineData(-9, false, "-09m")]
[InlineData(69, true, "+1h09")]
[InlineData(69, false, "1h09")]
[InlineData(-69, true, "-1h09")]
Expand All @@ -32,8 +33,8 @@ public void TimespanMultiply()
public void TimeSpanToHoursShouldWork(int timeInMinutes, bool showSign, string expected)
{
var timespan = TimeSpan.FromMinutes(timeInMinutes);
var initials = new TimeInitials("mn", "h", "j");
var result = TimeSpanExtensions.ToHours(timespan, initials, showSign);
var culture = CultureInfo.GetCultureInfo("fr-FR");
var result = TimeSpanExtensions.ToHours(timespan, culture, showSign);

Assert.Equal(expected, result);
}
Expand All @@ -53,8 +54,8 @@ public void TimeSpanToHoursShouldWork(int timeInMinutes, bool showSign, string e
public void TimeSpanToDaysShouldWork(int timeInMinutes, bool showSign, string expected)
{
var timespan = TimeSpan.FromMinutes(timeInMinutes);
var initials = new TimeInitials("mn", "h", "j");
var result = TimeSpanExtensions.ToDays(timespan, initials, showSign);
var culture = CultureInfo.GetCultureInfo("fr-FR");
var result = TimeSpanExtensions.ToDays(timespan, culture, showSign);

Assert.Equal(expected, result);
}
Expand Down
15 changes: 15 additions & 0 deletions NExtends/Primitives/DateTimes/ITimeBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace NExtends.Primitives.DateTimes
{
public interface ITimeBlock
{
DateTime StartsAt { get; }
DateTime EndsAt { get; }
TimeSpan Duration { get; }

void ChangeStartsAt(DateTime startsAt);
void ChangeEndsAt(DateTime endsAt);
void ChangeDuration(TimeSpan duration);
}
}
15 changes: 15 additions & 0 deletions NExtends/Primitives/DateTimes/NegativeDurationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Runtime.Serialization;

namespace NExtends.Primitives.DateTimes
{
[Serializable]
public class NegativeDurationException : ArgumentException
{
protected NegativeDurationException(SerializationInfo info, StreamingContext context)
: base(info, context) { }

public NegativeDurationException(string paramName)
: base("You cannot create or update a period to a negative duration", paramName) { }
}
}
74 changes: 70 additions & 4 deletions NExtends/Primitives/DateTimes/Period.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,82 @@

namespace NExtends.Primitives.DateTimes
{
public struct Period
public class Period : ITimeBlock, IEquatable<Period>
{
public DateTime Start { get; }
public DateTime End { get; }
public TimeSpan Duration { get { return End - Start; } }
public DateTime Start { get; private set; }
public DateTime End { get; private set; }
public TimeSpan Duration { get; private set; }

public DateTime StartsAt => Start;
public DateTime EndsAt => End;

void ITimeBlock.ChangeStartsAt(DateTime startsAt)
{
if (startsAt > End)
{
throw new NegativeDurationException(nameof(startsAt));
}

Start = startsAt;
}
void ITimeBlock.ChangeEndsAt(DateTime endsAt)
{
if (endsAt < Start)
{
throw new NegativeDurationException(nameof(endsAt));
}

End = endsAt;
}
void ITimeBlock.ChangeDuration(TimeSpan duration)
{
if (duration < TimeSpan.Zero)
{
throw new NegativeDurationException(nameof(duration));
}

Duration = duration;
}

public Period(DateTime start, DateTime end)
{
Start = start;
End = end;
Duration = end - start;

if (Duration < TimeSpan.Zero)
{
throw new NegativeDurationException(nameof(end));
}
}

public virtual bool Equals(Period other)
{
if (other == null) { return false; }

return Start == other.Start &&
End == other.End &&
Duration == other.Duration;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) { return false; }
if (ReferenceEquals(this, obj)) { return true; }
if (obj.GetType() != GetType()) { return false; }

return Equals(obj as Period);
}
public override int GetHashCode()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il est interdit d'implémenter GetHashCode() sur une une instance mutable, car la plupart des primitives de liste gardent le hashcode calculé en cache. Hors si l'instance est mutable, alors une fois modifiée, le HashCode n'est plus valide, mais il ne sera pas pour autant calculé.
C'est le genre d'effet de bord à se taper la tête contre les murs lors du débug.
D'autre part, pour des class, toujours implémenter un IEqualityComparer<T> plutôt que IEquatable<T>.

{
//cf http://www.aaronstannard.com/overriding-equality-in-dotnet/
unchecked
{
var hashCode = 13;
hashCode = (hashCode * 397) ^ Start.GetHashCode();
hashCode = (hashCode * 397) ^ End.GetHashCode();
hashCode = (hashCode * 397) ^ Duration.GetHashCode();
return hashCode;
}
}
}
}
21 changes: 20 additions & 1 deletion NExtends/Primitives/TimeSpans/TimeInitials.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
namespace NExtends.Primitives.TimeSpans
using System.Globalization;

namespace NExtends.Primitives.TimeSpans
{
public class TimeInitials
{
private static readonly TimeInitials _french = new TimeInitials("m", "h", "j");
private static readonly TimeInitials _german = new TimeInitials("M", "St", "T");
private static readonly TimeInitials _english = new TimeInitials("m", "h", "d");

public string MinutesInitial { get; }
public string HoursInitial { get; }
public string DaysInitial { get; }
Expand All @@ -12,5 +18,18 @@ public TimeInitials(string minutesInitial, string hoursInitial, string daysIniti
HoursInitial = hoursInitial;
DaysInitial = daysInitial;
}

public static TimeInitials FromCulture(CultureInfo culture)
{
switch(culture.Name)
{
case "fr-FR":
return _french;
case "de-DE":
return _german;
default:
return _english;
}
}
}
}
31 changes: 26 additions & 5 deletions NExtends/Primitives/TimeSpans/TimeSpan.extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,29 +46,39 @@ public static TimeSpan Sum<TSource>(this IEnumerable<TSource> source, Func<TSour
public static bool IsNegativeOrZero(this TimeSpan t1) { return t1.Ticks <= 0; }
public static bool IsNegative(this TimeSpan t1) { return t1.Ticks < 0; }

public static string Humanize(this TimeSpan timeSpan, TimeUnit timeUnit, TimeInitials initials, bool showSign = false)
public static string Humanize(this TimeSpan timeSpan, TimeUnit timeUnit, bool showSign = false)
{
return Humanize(timeSpan, timeUnit, CultureInfo.CurrentCulture, showSign);
}
public static string Humanize(this TimeSpan timeSpan, TimeUnit timeUnit, CultureInfo culture, bool showSign = false)
{
switch (timeUnit)
{
case TimeUnit.Day:
return ToDays(timeSpan, initials, showSign);
return ToDays(timeSpan, culture, showSign);
case TimeUnit.Duration:
case TimeUnit.Time:
return ToHours(timeSpan, initials, showSign);
return ToHours(timeSpan, culture, showSign);
case TimeUnit.NotApplicable:
default:
throw new InvalidEnumArgumentException(nameof(timeUnit));
}
}

public static string ToHours(this TimeSpan timeSpan, TimeInitials initials, bool showSign = false)
public static string ToHours(this TimeSpan timeSpan, bool showSign = false)
{
return ToHours(timeSpan, CultureInfo.CurrentCulture, showSign);
}
public static string ToHours(this TimeSpan timeSpan, CultureInfo culture, bool showSign = false)
{
if (timeSpan == TimeSpan.Zero)
{
return "-";
}

var absSpan = new TimeSpan(Math.Abs(timeSpan.Ticks));
var totalHours = Math.Floor(absSpan.TotalHours);
var initials = TimeInitials.FromCulture(culture);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on pourrait s'appuyer sur https://github.com/Humanizr/Humanizer pour ce genre de choses

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not, on peut faire ça dans une version ultérieure avec une Issue dédiée.


var sb = new StringBuilder();
if (showSign && timeSpan > TimeSpan.Zero)
Expand All @@ -91,17 +101,26 @@ public static string ToHours(this TimeSpan timeSpan, TimeInitials initials, bool
{
sb.Append(initials.MinutesInitial);
}

return sb.ToString();
}

public static string ToDays(this TimeSpan span, TimeInitials initials, bool showSign = false)
public static string ToDays(this TimeSpan span, bool showSign = false)
{
return ToDays(span, CultureInfo.CurrentCulture, showSign);
}

public static string ToDays(this TimeSpan span, CultureInfo culture, bool showSign = false)
{
if (span == TimeSpan.Zero)
{
return "-";
}

var absSpan = new TimeSpan(Math.Abs(span.Ticks));
var sb = new StringBuilder();
var initials = TimeInitials.FromCulture(culture);

if (showSign && span > TimeSpan.Zero)
{
sb.Append("+");
Expand All @@ -110,7 +129,9 @@ public static string ToDays(this TimeSpan span, TimeInitials initials, bool show
{
sb.Append("-");
}

sb.AppendFormat(CultureInfo.InvariantCulture, "{0} " + initials.DaysInitial, absSpan.TotalDays.RealRound(5));

return sb.ToString();
}
}
Expand Down