Browse Source

Add Snowflake class and Unit tests project

pull/1483/head
FiniteReality 5 years ago
parent
commit
72ab959e40
7 changed files with 242 additions and 0 deletions
  1. +2
    -0
      Directory.Build.targets
  2. +17
    -0
      Discord.Net.sln
  3. +70
    -0
      src/Core/Snowflake.cs
  4. +34
    -0
      test/Directory.Build.props
  5. +21
    -0
      test/Directory.Build.targets
  6. +13
    -0
      test/Discord.Net.Core.UnitTests/Discord.Net.Core.UnitTests.csproj
  7. +85
    -0
      test/Discord.Net.Core.UnitTests/SnowflakeTests.cs

+ 2
- 0
Directory.Build.targets View File

@@ -24,6 +24,8 @@
<PackageReference Update="Microsoft.Net.Compilers.Toolset" Version="3.6.0-2.final" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.0" />
<PackageReference Update="xunit" Version="2.4.1" />
<PackageReference Update="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

</Project>

+ 17
- 0
Discord.Net.sln View File

@@ -7,6 +7,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{381B0F15-BA2
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Core", "src\Core\Discord.Net.Core.csproj", "{57A52C6A-337D-4165-A42D-94FAC87B2807}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B960E106-DC21-4A28-9C28-6AA0B49346BB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Core.UnitTests", "test\Discord.Net.Core.UnitTests\Discord.Net.Core.UnitTests.csproj", "{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -32,8 +36,21 @@ Global
{57A52C6A-337D-4165-A42D-94FAC87B2807}.Release|x64.Build.0 = Release|Any CPU
{57A52C6A-337D-4165-A42D-94FAC87B2807}.Release|x86.ActiveCfg = Release|Any CPU
{57A52C6A-337D-4165-A42D-94FAC87B2807}.Release|x86.Build.0 = Release|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|x64.ActiveCfg = Debug|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|x64.Build.0 = Debug|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|x86.ActiveCfg = Debug|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Debug|x86.Build.0 = Debug|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|Any CPU.Build.0 = Release|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|x64.ActiveCfg = Release|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|x64.Build.0 = Release|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|x86.ActiveCfg = Release|Any CPU
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{57A52C6A-337D-4165-A42D-94FAC87B2807} = {381B0F15-BA2C-4E23-BE68-015462861AF0}
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534} = {B960E106-DC21-4A28-9C28-6AA0B49346BB}
EndGlobalSection
EndGlobal

+ 70
- 0
src/Core/Snowflake.cs View File

@@ -0,0 +1,70 @@
using System;

namespace Discord
{
/// <summary>
/// Utilities for reading and writing Discord snowflakes.
/// <seealso href="https://discordapp.com/developers/docs/reference#snowflakes"/>
/// </summary>
public static class Snowflake
{
/// <summary>
/// The offset, in milliseconds, from the Unix epoch which represents
/// the Discord Epoch.
/// </summary>
public const ulong DiscordEpochOffset = 1420070400000UL;

/// <summary>
/// Calculates the time a given snowflake was created.
/// </summary>
/// <param name="snowflake">
/// The snowflake to calculate the creation time of.
/// </param>
/// <returns>
/// A <see cref="DateTimeOffset"/> representing the creation time, in
/// UTC, of the snowflake.
/// </returns>
/// <example>
/// This sample demonstrates how to identify when a Discord user was
/// created.
/// <code>
/// IUser user = await GetUserAsync();
/// var snowflake = user.Id;
/// var created = Snowflake.GetCreatedTime(snowflake);
/// Console.WriteLine($"The user {user.Name} was created at {created}");
/// </code>
/// </example>
public static DateTimeOffset GetCreatedTime(ulong snowflake)
=> DateTimeOffset.FromUnixTimeMilliseconds(
(long)((snowflake >> 22) + DiscordEpochOffset));

/// <summary>
/// Calculates the smallest possible snowflake for a given creation
/// time.
/// </summary>
/// <param name="time">
/// The time to generate a snowflake for.
/// </param>
/// <returns>
/// A snowflake representing the smallest possible snowflake for the
/// given creation time.
/// </returns>
/// <example>
/// This sample demonstrates how to check if a user was created before
/// a certain date.
/// <code>
/// IUser user = await GetUserAsync();
/// var desiredTime = DateTimeOffset.UtcNow.AddDays(-7);
/// var minimumSnowflake = Snowflake.GetSnowflake(desiredTime);
///
/// if (user.Id &lt;= minimumSnowflake)
/// Console.WriteLine($"The user {user.Name} was created at least 7 days ago");
/// else
/// Console.WriteLine($"The user {user.Name} was created less than 7 days ago");
/// </code>
/// </example>
public static ulong GetSnowflake(DateTimeOffset time)
=> ((ulong)time.ToUnixTimeMilliseconds()
- DiscordEpochOffset) << 22;
}
}

+ 34
- 0
test/Directory.Build.props View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Based on https://github.com/terrafx/terrafx/blob/master/Directory.Build.props -->
<!-- Copyright © Tanner Gooding and Contributors -->
<Project>

<!--
Directory.Build.props is automatically picked up and imported by
Microsoft.Common.props. This file needs to exist, even if empty so that
files in the parent directory tree, with the same name, are not imported
instead. The import fairly early and only Sdk.props will have been
imported beforehand. We also don't need to add ourselves to
MSBuildAllProjects, as that is done by the file that imports us.
-->

<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.props</MSBuildAllProjects>
<DiscordNetProjectCategory>test</DiscordNetProjectCategory>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.props" />

<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<VSTestLogger>trx</VSTestLogger>
<VSTestResultsDirectory>$(BaseArtifactsPath)tst/$(Configuration)/</VSTestResultsDirectory>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" IsImplicitlyDefined="true" PrivateAssets="all" />
<PackageReference Include="xunit" IsImplicitlyDefined="true" PrivateAssets="all" />
<PackageReference Include="xunit.runner.visualstudio" IsImplicitlyDefined="true" PrivateAssets="all" />
</ItemGroup>

</Project>

+ 21
- 0
test/Directory.Build.targets View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Based on https://github.com/terrafx/terrafx/blob/master/Directory.Build.props -->
<!-- Copyright © Tanner Gooding and Contributors -->
<Project>

<!--
Directory.Build.targets is automatically picked up and imported by
Microsoft.Common.targets. This file needs to exist, even if empty so that
files in the parent directory tree, with the same name, are not imported
instead. The import fairly late and most other props/targets will have
been imported beforehand. We also don't need to add ourselves to
MSBuildAllProjects, as that is done by the file that imports us.
-->

<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.targets</MSBuildAllProjects>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.targets" />

</Project>

+ 13
- 0
test/Discord.Net.Core.UnitTests/Discord.Net.Core.UnitTests.csproj View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Core\Discord.Net.Core.csproj" />
</ItemGroup>

</Project>

+ 85
- 0
test/Discord.Net.Core.UnitTests/SnowflakeTests.cs View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using Xunit;

namespace Discord.UnitTests
{
public class SnowflakeTests
{
private static IEnumerable<object[]> GetTimestampTestData()
{
// N.B. snowflakes here should have the least significant 22 bits
// set to zero.
yield return new object[]
{
81062087257751552UL,
new DateTimeOffset(
year: 2015, month: 08, day: 12,
hour: 16, minute: 31, second: 47,
millisecond: 663,
offset: TimeSpan.Zero),
};

yield return new object[]
{
0UL,
new DateTimeOffset(
year: 2015, month: 1, day: 1,
hour: 0, minute: 0, second: 0,
millisecond: 0,
offset: TimeSpan.Zero)
};

yield return new object[]
{
(ulong.MaxValue >> 22) << 22,
new DateTimeOffset(
year: 2154, month: 05, day: 15,
hour: 07, minute: 35, second: 11,
millisecond: 103,
offset: TimeSpan.Zero)
};
}

private static IEnumerable<object[]> GetRoundtrippableTestData()
{
// N.B. snowflakes here should have the least significant 22 bits
// set to zero.

yield return new object[]{ 81062087257751552UL };
yield return new object[]{ 0UL };
yield return new object[]{ (ulong.MaxValue >> 22) << 22 };
}

[Theory]
[MemberData(nameof(GetTimestampTestData))]
public void SnowflakeExpectedTimestamp(
ulong snowflake, DateTimeOffset expected)
{
var time = Snowflake.GetCreatedTime(snowflake);

Assert.Equal(time, expected);
}

[Theory]
[MemberData(nameof(GetTimestampTestData))]
public void SnowflakeExpectedSnowflake(
ulong expected, DateTimeOffset time)
{
var snowflake = Snowflake.GetSnowflake(time);

Assert.Equal(expected, snowflake);
}

[Theory]
[MemberData(nameof(GetRoundtrippableTestData))]
public void SnowflakeIsRoundTrippable(
ulong expected)
{
var time = Snowflake.GetCreatedTime(expected);
var roundtripped = Snowflake.GetSnowflake(time);

Assert.Equal(expected, roundtripped);
}
}
}

Loading…
Cancel
Save