Skip to content

Commit

Permalink
Handle negative time span (#328)
Browse files Browse the repository at this point in the history
  • Loading branch information
CoryCharlton authored May 17, 2024
1 parent 16a7727 commit eff5937
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 95 deletions.
78 changes: 65 additions & 13 deletions nanoFramework.Json.Test/Converters/TimeSpanConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See LICENSE file in the project root for full license information.
//

using nanoFramework.Json.Converters;
using nanoFramework.TestFramework;
using System;

Expand All @@ -12,26 +13,77 @@ namespace nanoFramework.Json.Test.Converters
public class TimeSpanConverterTests
{
[TestMethod]
[DataRow("10:00:00", 10)]
[DataRow("24:00:00", 24)]
public void TimeSpanConverter_ToType_ShouldReturnValidData(string value, int expectedValueHours)
public void TimeSpanConverter_ToType_Should_Return_Valid_Data()
{
var converter = new Json.Converters.TimeSpanConverter();
var convertedValue = (TimeSpan)converter.ToType(value);
var values = new[]
{
"-1.02:03:04.005",
"1.02:03:04.0050000",
"4.03:02:01.654321",
"4.03:02:01.65432",
"4.03:02:01.6543",
"4.03:02:01.654",
"4.03:02:01.65",
"4.03:02:01.6",
"04:20:19",
"07:32",
};

var expectedTimeSpanValue = TimeSpan.FromHours(expectedValueHours);
Assert.AreEqual(expectedTimeSpanValue.Ticks, convertedValue.Ticks);
var expected = new[]
{
-new TimeSpan(1, 2, 3, 4, 5),
new TimeSpan(1, 2, 3, 4, 5),
new TimeSpan(4, 3, 2, 1, 654).Add(new TimeSpan(3210)),
new TimeSpan(4, 3, 2, 1, 654).Add(new TimeSpan(3200)),
new TimeSpan(4, 3, 2, 1, 654).Add(new TimeSpan(3000)),
new TimeSpan(4, 3, 2, 1, 654),
new TimeSpan(4, 3, 2, 1, 650),
new TimeSpan(4, 3, 2, 1, 600),
new TimeSpan(4, 20, 19),
new TimeSpan(7, 32, 0),
};

var sut = new TimeSpanConverter();

for (var i = 0; i < values.Length; i++)
{
var actual = (TimeSpan) sut.ToType(values[i]);

Assert.AreEqual(expected[i], actual);
}
}

[TestMethod]
[DataRow(10, "\"10:00:00\"")]
[DataRow(24, "\"24:00:00\"")]
public void TimeSpanConverter_ToJson_Should_ReturnValidData(int valueHours, string expectedValue)
public void TimeSpanConverter_ToJson_Should_Return_Valid_Data()
{
var converter = new Json.Converters.TimeSpanConverter();
var convertedValue = converter.ToJson(TimeSpan.FromHours(valueHours));
var values = new[]
{
-new TimeSpan(1, 2, 3, 4, 5),
new TimeSpan(1, 2, 3, 4, 5),
new TimeSpan(4, 3, 2, 1, 654).Add(new TimeSpan(3210)),
new TimeSpan(4, 20, 19),
new TimeSpan(7, 32, 0),
new TimeSpan(0, 29, 0),
};

var expected = new[]
{
"\"-1.02:03:04.0050000\"",
"\"1.02:03:04.0050000\"",
"\"4.03:02:01.6543210\"",
"\"04:20:19\"",
"\"07:32:00\"",
"\"00:29:00\"",
};

var sut = new TimeSpanConverter();

for (var i = 0; i < values.Length; i++)
{
var actual = sut.ToJson(values[i]);

Assert.AreEqual(expectedValue, convertedValue);
Assert.AreEqual(expected[i], actual);
}
}
}
}
30 changes: 12 additions & 18 deletions nanoFramework.Json.Test/nanoFramework.Json.Test.nfproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,40 +59,34 @@
<Compile Include="Resolvers\MemberResolverCaseSensitiveTests.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="mscorlib, Version=1.15.6.0, Culture=neutral, PublicKeyToken=c07d481e9758c731">
<Content Include="packages.lock.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\nanoFramework.Json\nanoFramework.Json.nfproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="mscorlib">
<HintPath>..\packages\nanoFramework.CoreLibrary.1.15.5\lib\mscorlib.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="nanoFramework.System.Collections, Version=1.5.31.0, Culture=neutral, PublicKeyToken=c07d481e9758c731">
<Reference Include="nanoFramework.System.Collections">
<HintPath>..\packages\nanoFramework.System.Collections.1.5.31\lib\nanoFramework.System.Collections.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="nanoFramework.System.Text, Version=1.2.54.0, Culture=neutral, PublicKeyToken=c07d481e9758c731">
<Reference Include="nanoFramework.System.Text">
<HintPath>..\packages\nanoFramework.System.Text.1.2.54\lib\nanoFramework.System.Text.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="nanoFramework.TestFramework, Version=2.1.94.0, Culture=neutral, PublicKeyToken=c07d481e9758c731">
<Reference Include="nanoFramework.TestFramework">
<HintPath>..\packages\nanoFramework.TestFramework.2.1.94\lib\nanoFramework.TestFramework.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="nanoFramework.UnitTestLauncher, Version=0.0.0.0, Culture=neutral, PublicKeyToken=c07d481e9758c731">
<Reference Include="nanoFramework.UnitTestLauncher">
<HintPath>..\packages\nanoFramework.TestFramework.2.1.94\lib\nanoFramework.UnitTestLauncher.exe</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.IO.Streams, Version=1.1.59.0, Culture=neutral, PublicKeyToken=c07d481e9758c731">
<Reference Include="System.IO.Streams">
<HintPath>..\packages\nanoFramework.System.IO.Streams.1.1.59\lib\System.IO.Streams.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\nanoFramework.Json\nanoFramework.Json.nfproj" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="packages.lock.json" />
</ItemGroup>
<Import Project="..\nanoFramework.Json.Test.Shared\nanoFramework.Json.Test.Shared.projitems" Label="Shared" />
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.CSharp.targets" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.CSharp.targets')" />
<!-- MANUAL UPDATE HERE -->
Expand Down
129 changes: 65 additions & 64 deletions nanoFramework.Json/Converters/TimeSpanConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ internal sealed class TimeSpanConverter : IConverter
/// <summary>
/// <inheritdoc/>
/// </summary>
public string ToJson(object value)
{
return "\"" + value.ToString() + "\"";
}
public string ToJson(object value) => $"\"{value}\"";

/// <summary>
/// <inheritdoc/>
Expand All @@ -34,32 +31,34 @@ internal static TimeSpan ConvertFromString(string value)
{
// split string value with all possible separators
// format is: -ddddd.HH:mm:ss.fffffff
var timeSpanBits = value.Split(':', '.');
var tokens = value.Split(':', '.');

// sanity check
if (timeSpanBits.Length == 0)
if (tokens.Length == 0)
{
return TimeSpan.Zero;
}

// figure out where the separators are
int indexOfFirstDot = value.IndexOf('.');
int indexOfSecondDot = indexOfFirstDot > -1 ? value.IndexOf('.', indexOfFirstDot + 1) : -1;
int indexOfFirstColon = value.IndexOf(':');
int indexOfSecondColon = indexOfFirstColon > -1 ? value.IndexOf(':', indexOfFirstColon + 1) : -1;
var indexOfFirstDot = value.IndexOf('.');
var indexOfSecondDot = indexOfFirstDot > -1 ? value.IndexOf('.', indexOfFirstDot + 1) : -1;
var indexOfFirstColon = value.IndexOf(':');
var indexOfSecondColon = indexOfFirstColon > -1 ? value.IndexOf(':', indexOfFirstColon + 1) : -1;

// sanity check for separators: all have to be ahead of string start
if (SeparatorCheck(timeSpanBits, indexOfFirstDot, indexOfSecondDot, indexOfFirstColon, indexOfSecondColon))
if (SeparatorCheck(tokens, indexOfFirstDot, indexOfSecondDot, indexOfFirstColon, indexOfSecondColon))
{
throw new InvalidCastException();
}

var isNegative = value.StartsWith("-");

// to have days, it has to have something before the 1st dot, or just have a single component
bool hasDays = (indexOfFirstDot > 0 && indexOfFirstDot < indexOfFirstColon) || timeSpanBits.Length == 1;
bool hasTicks = hasDays ? indexOfSecondDot > indexOfFirstDot : indexOfFirstDot > -1;
bool hasHours = indexOfFirstColon > 0;
bool hasMinutes = hasHours && indexOfFirstColon > -1;
bool hasSeconds = hasMinutes && indexOfSecondColon > -1;
var hasDays = (indexOfFirstDot > 0 && indexOfFirstDot < indexOfFirstColon) || tokens.Length == 1;
var hasTicks = hasDays ? indexOfSecondDot > indexOfFirstDot : indexOfFirstDot > -1;
var hasHours = indexOfFirstColon > 0;
var hasMinutes = hasHours && indexOfFirstColon > -1;
var hasSeconds = hasMinutes && indexOfSecondColon > -1;

// sanity check for ticks without other time components
if (hasTicks && !hasHours)
Expand All @@ -68,20 +67,13 @@ internal static TimeSpan ConvertFromString(string value)
}

// let the parsing start!
int days = 0;
if (hasDays
&& !int.TryParse(timeSpanBits[0], out days))
{
throw new InvalidCastException();
}
var tokenIndex = 0;

// bump the index if days component is present
int processIndex = hasDays ? 1 : 0;

var hours = ParseValueFromString(hasHours, timeSpanBits, ref processIndex);
var minutes = ParseValueFromString(hasMinutes, timeSpanBits, ref processIndex);
var seconds = ParseValueFromString(hasSeconds, timeSpanBits, ref processIndex);
var ticks = HandleTicks(timeSpanBits, hasTicks, processIndex);
var days = ParseToken(hasDays, tokens, ref tokenIndex);
var hours = ParseToken(hasHours, tokens, ref tokenIndex);
var minutes = ParseToken(hasMinutes, tokens, ref tokenIndex);
var seconds = ParseToken(hasSeconds, tokens, ref tokenIndex);
var ticks = ParseTicks(hasTicks, tokens, tokenIndex);

// sanity check for valid ranges
if (IsInvalidTimeSpan(hours, minutes, seconds))
Expand All @@ -90,77 +82,86 @@ internal static TimeSpan ConvertFromString(string value)
}

// we should have everything now
return new TimeSpan(ticks).Add(new TimeSpan(days, hours, minutes, seconds, 0));
var timeSpan = new TimeSpan(days, hours, minutes, seconds, 0).Add(new TimeSpan(ticks));

return isNegative ? -timeSpan : timeSpan;
}

private static int HandleTicks(string[] timeSpanBits, bool hasTicks, int processIndex)
private static bool IsInvalidTimeSpan(int hour, int minutes, int seconds)
{
if (!hasTicks || processIndex > timeSpanBits.Length)
if (hour is < 0 or > 24)
{
return 0;
return true;
}

if (!int.TryParse(timeSpanBits[processIndex], out var ticks))
if (minutes is < 0 or >= 60)
{
throw new InvalidCastException();
return true;
}

// if ticks are under 999, that's milliseconds
if (ticks < 1_000)
if (seconds is < 0 or >= 60)
{
ticks *= 10_000;
return true;
}

return ticks;
}

private static bool SeparatorCheck(string[] timeSpanBits, int indexOfFirstDot, int indexOfSecondDot, int indexOfFirstColon, int indexOfSecondColon)
{
return timeSpanBits.Length > 1
&& indexOfFirstDot <= 0
&& indexOfSecondDot <= 0
&& indexOfFirstColon <= 0
&& indexOfSecondColon <= 0;
return false;
}

private static int ParseValueFromString(bool hasValue,string[] timeSpanBits, ref int processIndex)
private static int ParseTicks(bool hasTicks, string[] tokens, int tokenIndex)
{
if (!hasValue)
if (!hasTicks || tokenIndex > tokens.Length)
{
return 0;
}

if (processIndex > timeSpanBits.Length)
var token = tokens[tokenIndex];

if (token.Length > 7)
{
return 0;
token = token.Substring(0, 7);
}

if (!int.TryParse(timeSpanBits[processIndex++], out var value))
if (!int.TryParse(token, out var value))
{
throw new InvalidCastException();
}

return value;
value = token.Length switch
{
1 => value * 1_000_000,
2 => value * 100_000,
3 => value * 10_000,
4 => value * 1_000,
5 => value * 100,
6 => value * 10,
_ => value
};

return value >= 0 ? value : value * -1;
}

private static bool IsInvalidTimeSpan(int hour, int minutes, int seconds)
private static int ParseToken(bool hasValue, string[] tokens, ref int tokenIndex)
{
if (hour < 0 || hour > 24)
if (!hasValue || tokenIndex > tokens.Length)
{
return true;
return 0;
}

if (minutes < 0 || minutes >= 60)
if (!int.TryParse(tokens[tokenIndex++], out var value))
{
return true;
throw new InvalidCastException();
}

if (seconds < 0 || seconds >= 60)
{
return true;
}
return value >= 0 ? value : value * -1;
}

return false;
private static bool SeparatorCheck(string[] tokens, int indexOfFirstDot, int indexOfSecondDot, int indexOfFirstColon, int indexOfSecondColon)
{
return tokens.Length > 1
&& indexOfFirstDot <= 0
&& indexOfSecondDot <= 0
&& indexOfFirstColon <= 0
&& indexOfSecondColon <= 0;
}
}
}

0 comments on commit eff5937

Please sign in to comment.