@@ -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> |
@@ -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 |
@@ -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 <= 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; | |||
} | |||
} |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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); | |||
} | |||
} | |||
} |