@@ -24,6 +24,8 @@ | |||||
<PackageReference Update="Microsoft.Net.Compilers.Toolset" Version="3.6.0-2.final" /> | <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.NET.Test.Sdk" Version="16.5.0" /> | ||||
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.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> | </ItemGroup> | ||||
</Project> | </Project> |
@@ -7,6 +7,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{381B0F15-BA2 | |||||
EndProject | EndProject | ||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Core", "src\Core\Discord.Net.Core.csproj", "{57A52C6A-337D-4165-A42D-94FAC87B2807}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Core", "src\Core\Discord.Net.Core.csproj", "{57A52C6A-337D-4165-A42D-94FAC87B2807}" | ||||
EndProject | 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 | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
Debug|Any CPU = Debug|Any CPU | 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|x64.Build.0 = Release|Any CPU | ||||
{57A52C6A-337D-4165-A42D-94FAC87B2807}.Release|x86.ActiveCfg = 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 | {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 | EndGlobalSection | ||||
GlobalSection(NestedProjects) = preSolution | GlobalSection(NestedProjects) = preSolution | ||||
{57A52C6A-337D-4165-A42D-94FAC87B2807} = {381B0F15-BA2C-4E23-BE68-015462861AF0} | {57A52C6A-337D-4165-A42D-94FAC87B2807} = {381B0F15-BA2C-4E23-BE68-015462861AF0} | ||||
{1A1D1A6F-F9DD-4A14-9F93-9B4C88F0B534} = {B960E106-DC21-4A28-9C28-6AA0B49346BB} | |||||
EndGlobalSection | EndGlobalSection | ||||
EndGlobal | 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); | |||||
} | |||||
} | |||||
} |