Permalink
Browse files

Give each release a structured version number, for better ordering

Fixes #905.
  • Loading branch information...
jskeet committed Aug 14, 2017
1 parent d7fe2ad commit dcc07adf089832b49f986d4b301aa7865c565d10
@@ -0,0 +1,73 @@
// Copyright 2017 The Noda Time Authors. All rights reserved.
// Use of this source code is governed by the Apache License 2.0,
// as found in the LICENSE.txt file.using System;
using NodaTime.Web.Models;
using NUnit.Framework;
using System;
namespace NodaTime.Web.Test.Models
{
public class StructuredVersionTest
{
[Test]
[TestCase("1.0.0", "1.0.1")]
[TestCase("1.0.0", "1.1.0")]
[TestCase("1.1.0", "2.0.0")]
[TestCase("1.0.0", "1.0.1-alpha01")]
[TestCase("1.0.0-alpha01", "1.0.0-alpha02")]
[TestCase("1.0.0-alpha01", "1.0.0-beta01")]
[TestCase("1.0.0-alpha01", "1.0.0")]
[TestCase("2.0.0", "10.0.0")]
[TestCase("1.2.0", "1.10.0")]
[TestCase("1.0.2", "1.0.10")]
public void Compare(string earlier, string later)
{
StructuredVersion earlierVersion = new StructuredVersion(earlier);
StructuredVersion laterVersion = new StructuredVersion(later);
Assert.That(earlierVersion.CompareTo(laterVersion), Is.LessThan(0));
Assert.That(laterVersion.CompareTo(earlierVersion), Is.GreaterThan(0));
Assert.AreNotEqual(laterVersion, earlierVersion);
Assert.AreNotEqual(laterVersion.GetHashCode(), earlierVersion.GetHashCode());
}
[Test]
[TestCase("1.2.3")]
[TestCase("1.2.3-alpha01")]
public void Compare_Equal(string value)
{
StructuredVersion v1 = new StructuredVersion(value);
StructuredVersion v2 = new StructuredVersion(value);
Assert.AreEqual(0, v1.CompareTo(v2));
Assert.AreEqual(v1, v2);
Assert.AreEqual(v1.GetHashCode(), v2.GetHashCode());
}
[Test]
[TestCase("1.2.3", 1, 2, 3, null)]
[TestCase("11.22.33-xyz", 11, 22, 33, "xyz")]
public void Construction(string version, int major, int minor, int patch, string prerelease)
{
var structured = new StructuredVersion(version);
Assert.AreEqual(major, structured.Major);
Assert.AreEqual(minor, structured.Minor);
Assert.AreEqual(patch, structured.Patch);
Assert.AreEqual(prerelease, structured.Prerelease);
}
[Test]
[TestCase("")]
[TestCase("abc")]
[TestCase("1")]
[TestCase("1.0")]
[TestCase("1.0.0+other")]
[TestCase("1.0.0other")]
[TestCase("1.0.0-")]
[TestCase("1.0.9999999999999999")]
[TestCase("x1.0.0y")]
public void Construction_Invalid(string version)
{
Assert.Throws<ArgumentException>(() => new StructuredVersion(version));
}
}
}
@@ -11,13 +11,4 @@ public static int Main(string[] args)
return new AutoRun(typeof(Program).GetTypeInfo().Assembly).Execute(args);
}
}
// Fake test just so that there's something to test in the first commit.
public class DummyTest
{
[Test]
public void Dummy()
{
}
}
}
@@ -12,7 +12,7 @@ public class FakeReleaseRepository : IReleaseRepository
{
private static readonly IList<ReleaseDownload> releases = new[]
{
new ReleaseDownload("2.0.0", "NodaTime-2.0.0.zip",
new ReleaseDownload(new StructuredVersion("2.0.0"), "NodaTime-2.0.0.zip",
"https://storage.cloud.google.com/nodatime/releases/NodaTime-2.0.0.zip",
"36c6e7b4c10ba21e39b8652987e5c8c0f46a3f03f83f5265bf2893c8837cf635",
new LocalDate(2017, 3, 31))
@@ -43,7 +43,7 @@ private CacheValue FetchReleases()
.ListObjects(Bucket, ObjectPrefix)
.Where(obj => !obj.Name.EndsWith("/"))
.Select(ConvertObject)
.OrderByDescending(r => r.Release)
.OrderByDescending(r => r.Version)
.ToList();
return new CacheValue(releases);
}
@@ -55,15 +55,11 @@ private static ReleaseDownload ConvertObject(Google.Apis.Storage.v1.Data.Object
string releaseDateMetadata = null;
obj.Metadata?.TryGetValue(ReleaseDateKey, out releaseDateMetadata);
var match = ReleasePattern.Match(obj.Name);
string release = null;
if (match.Success)
{
release = match.Groups[1].Value;
}
StructuredVersion version = match.Success ? new StructuredVersion(match.Groups[1].Value) : null;
LocalDate releaseDate = releaseDateMetadata == null
? LocalDate.FromDateTime(obj.Updated.Value)
: LocalDatePattern.Iso.Parse(releaseDateMetadata).Value;
return new ReleaseDownload(release, obj.Name.Substring(ObjectPrefix.Length), $"https://storage.googleapis.com/{Bucket}/{obj.Name}", sha256Hash, releaseDate);
return new ReleaseDownload(version, obj.Name.Substring(ObjectPrefix.Length), $"https://storage.googleapis.com/{Bucket}/{obj.Name}", sha256Hash, releaseDate);
}
private class CacheValue
@@ -72,7 +68,7 @@ private class CacheValue
public ReleaseDownload LatestRelease { get; }
public static CacheValue Empty { get; } =
new CacheValue(new List<ReleaseDownload> { new ReleaseDownload("Dummy", "Dummy", "", "", new LocalDate(2000, 1, 1)) });
new CacheValue(new List<ReleaseDownload> { new ReleaseDownload(null, "Dummy", "", "", new LocalDate(2000, 1, 1)) });
public CacheValue(List<ReleaseDownload> releases)
{
@@ -81,7 +77,7 @@ public CacheValue(List<ReleaseDownload> releases)
// 1.4 comes out after 2.0, 2.0 is still latest.)
LatestRelease = releases
.Where(r => !r.File.Contains("-src"))
.OrderByDescending(r => r.Release)
.OrderByDescending(r => r.Version)
.First();
}
}
@@ -8,15 +8,15 @@ namespace NodaTime.Web.Models
{
public class ReleaseDownload
{
public string Release { get; }
public StructuredVersion Version { get; }
public string File { get; }
public string DownloadUrl { get; }
public string Sha256Hash { get; }
public LocalDate ReleaseDate { get; set; }
public ReleaseDownload(string release, string file, string downloadUrl, string sha256Hash, LocalDate releaseDate)
public ReleaseDownload(StructuredVersion version, string file, string downloadUrl, string sha256Hash, LocalDate releaseDate)
{
Release = release;
Version = version;
File = file;
DownloadUrl = downloadUrl;
Sha256Hash = sha256Hash;
@@ -0,0 +1,71 @@
// Copyright 2017 The Noda Time Authors. All rights reserved.
// Use of this source code is governed by the Apache License 2.0,
// as found in the LICENSE.txt file.using System;
using System;
using System.Text.RegularExpressions;
namespace NodaTime.Web.Models
{
/// <summary>
/// A structured version number: major/minor/patch/pre-release
/// </summary>
public class StructuredVersion : IComparable<StructuredVersion>, IEquatable<StructuredVersion>
{
// Allow up to 7 digits per part. That's enough for anything sensible, and will avoid overflow when parsing.
private static readonly Regex VersionPattern = new Regex(@"^(?<major>\d{1,7})\.(?<minor>\d{1,7})\.(?<patch>\d{1,7})(?:-(?<pre>.+))?$");
private readonly string text;
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
/// <summary>
/// Prerelease part of the version, if any, e.g. "beta01". Null for GA versions.
/// </summary>
public string Prerelease { get; }
public override string ToString() => text;
public StructuredVersion(string version)
{
text = version; // TODO: Normalize?
var match = VersionPattern.Match(version);
if (!match.Success)
{
throw new ArgumentException($"String '{version}' isn't a valid version", nameof(version));
}
Major = int.Parse(match.Groups["major"].Value);
Minor = int.Parse(match.Groups["minor"].Value);
Patch = int.Parse(match.Groups["patch"].Value);
Prerelease = match.Groups["pre"].Value;
if (Prerelease == "")
{
Prerelease = null;
}
}
public int CompareTo(StructuredVersion other) =>
other == null ? -1
: Major != other.Major ? Major.CompareTo(other.Major)
: Minor != other.Minor ? Minor.CompareTo(other.Minor)
: Patch != other.Patch ? Patch.CompareTo(other.Patch)
: ComparePrereleases(Prerelease, other.Prerelease);
// A null prerelease needs to come *before* a non-null one
private static int ComparePrereleases(string x, string y) =>
x == y ? 0
: x == null ? 1
: y == null ? -1
: string.CompareOrdinal(x, y);
public bool Equals(StructuredVersion other) => other != null &&
other.Major == Major &&
other.Minor == Minor &&
other.Patch == Patch &&
other.Prerelease == Prerelease;
public override bool Equals(object obj) => Equals(obj as StructuredVersion);
public override int GetHashCode() => (Major * 100 + Minor * 10 + Patch) ^ (Prerelease?.GetHashCode() ?? 0);
}
}
@@ -28,7 +28,7 @@
<th>Downloads</th>
<th>SHA-256</th>
</tr>
@foreach (var release in Model.GroupBy(r => r.Release).OrderByDescending(g => g.Key))
@foreach (var release in Model.GroupBy(r => r.Version).OrderByDescending(g => g.Key))
{
bool first = true;
@foreach (var file in release.OrderBy(f => f.File))
@@ -113,7 +113,7 @@
<div class="small-6 columns">
@{
var latest = @releases.LatestRelease;
<span>The latest version is @latest.Release, released @latest.ReleaseDate.ToString("dd MMMM yyyy", CultureInfo.InvariantCulture).</span>
<span>The latest version is @latest.Version, released @latest.ReleaseDate.ToString("dd MMMM yyyy", CultureInfo.InvariantCulture).</span>
}
</div>
<div class="small-6 columns copy">

0 comments on commit dcc07ad

Please sign in to comment.