Class/SemVerRange.cs
using System;
using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace pspm { //:------------------------: #region SemVerRange //:------------------------: /// <summary> /// The SemVerRange class express a range of SemVer, /// allowing you to parse range expression string, /// and allowing you to test a SemVer satisfies the range or not. /// </summary> /// <example> /// <code> /// var Range = new SemVerRange("1.2.x"); /// Range.IsSatisfied('1.2.0'); // =>true /// Range.IsSatisfied('1.3.0'); // =>false /// </code> /// </example> /// <seealso> /// You can refer to the range syntax in the document of npm-semver. /// (Some advanced syntax is not implemented yet, sorry.) /// https://docs.npmjs.com/misc/semver /// </seealso> public class SemVerRange { /// <summary> /// The minimum version of range /// </summary> public SemVer MinimumVersion { get; private set; } /// <summary> /// The maximum version of range /// </summary> public SemVer MaximumVersion { get; private set; } /// <summary> /// Whether or not to include the maximum version /// </summary> public bool IncludeMinimum { get; private set; } /// <summary> /// Whether or not to include the minimum version /// </summary> public bool IncludeMaximum { get; private set; } /// <summary> /// Expression string of range /// </summary> public string Expression { get; private set; } public SemVerRange[] RangeSet { get; private set; } /// <summary> /// Construct a new range that not matched any of versions (<0.0.0) /// </summary> public SemVerRange() : this(SemVer.Min, SemVer.Min, false, false) { this.Expression = "<0.0.0"; } /// <summary> /// Construct a new range from a min & max version /// </summary> /// <param name="min">The minimum version of a range (>=min)</param> /// <param name="max">The maximum version of a range (<=max)</param> public SemVerRange(SemVer min, SemVer max) : this(min, max, true, true) { } /// <summary> /// Construct a new range from a min & max version /// </summary> /// <param name="min">The minimum version of a range (&ft;=min)</param> /// <param name="max">The maximum version of a range (<=max)</param> /// <param name="includeMin">Whether or not to include the minimum version</param> /// <param name="includeMax">Whether or not to include the maximum version</param> public SemVerRange(SemVer min, SemVer max, bool includeMin, bool includeMax) { this.MaximumVersion = max; this.MinimumVersion = min; this.IncludeMaximum = includeMax; this.IncludeMinimum = includeMin; var opmin = (includeMin == true) ? ">=" : ">"; var opmax = (includeMax == true) ? "<=" : "<"; if (max == SemVer.Max && includeMax == true) { this.Expression = $"{opmin}{min.ToString()}"; } else if (min == SemVer.Min && includeMin == true) { this.Expression = $"{opmax}{max.ToString()}"; } else { this.Expression = $"{opmin}{min.ToString()} {opmax}{max.ToString()}"; } this.RangeSet = new SemVerRange[] { this }; } /// <summary> /// Construct a new range from range expression string /// </summary> /// <param name="expression">The range expression string</param> /// <exception cref="System.ArgumentException">Thrown when the range expression is invalid or unsupported</exception> public SemVerRange(string expression) { // next foreach throws exception when expression == "". it's dirty workaround expression = (!string.IsNullOrWhiteSpace(expression)) ? expression : "*"; List<SemVerRange> rangeSet = new List<SemVerRange>(); List<string> allExpressions = Regex.Split(expression.Trim(), @"\|\|").Select(e => e.Trim()).Where(e => !string.IsNullOrEmpty(e)).ToList(); foreach (var subexp in allExpressions) { string[] subIntersection = Regex.Split(subexp, @"(?<!-)\s+(?!-)").Select(s => s.Trim()).Where(e => !string.IsNullOrEmpty(e)).ToArray(); List<SemVerRange> intersectionSet = new List<SemVerRange>(); foreach (var sub in subIntersection) { intersectionSet.Add(Parse(sub)); } rangeSet.Add(IntersectAll(intersectionSet.ToArray())); } SemVerRange range = _UnionAll(rangeSet.ToArray()); this.MinimumVersion = range.MinimumVersion; this.MaximumVersion = range.MaximumVersion; this.IncludeMinimum = range.IncludeMinimum; this.IncludeMaximum = range.IncludeMaximum; this.Expression = range.Expression; this.RangeSet = range.RangeSet; } private static SemVerRange _newSemVerRangeSet(params SemVerRange[] ranges) { if (ranges == null || ranges.Length < 1) { throw new ArgumentException(); } SemVerRange result = new SemVerRange(); if (ranges.Length >= 1) { result.MinimumVersion = ranges[0].MinimumVersion; result.MaximumVersion = ranges[0].MaximumVersion; result.IncludeMinimum = ranges[0].IncludeMinimum; result.IncludeMaximum = ranges[0].IncludeMaximum; result.Expression = ranges[0].Expression; result.RangeSet = ranges; } if (ranges.Length >= 2) { result.Expression = string.Join(" || ", ranges.Select(e => e.Expression)); } return result; } // Parse() private static SemVerRange Parse(string expression) { var isError = false; SemVerRange range = new SemVerRange(); //All if (string.IsNullOrEmpty(expression) || expression.Equals("*")) { // empty or asterisk match all versions range.MaximumVersion = SemVer.Max; range.MinimumVersion = SemVer.Min; range.IncludeMaximum = true; range.IncludeMinimum = true; range.Expression = ">=0.0.0"; } // Hyphen Ranges (1.2.3 - 2.3.4, 1.2 - 2.3.4, 1.2.3 - 2) else if (Regex.IsMatch(expression, @"^.+ - .+$", RegexOptions.IgnoreCase)) { string[] a = Regex.Split(expression, @" - "); string first = a[0]; string second = a[1]; Regex valid = new Regex(@"^[=vV]*\d+(\.\d+){0,3}$"); Regex partial = new Regex(@"^[=vV]*\d+(\.\d+)?$"); if (!valid.IsMatch(first) || !valid.IsMatch(second)) { isError = true; } else { //>=first SemVerRange firstRange = new SemVerRange(new SemVer(first), SemVer.Max, true, true); SemVerRange secondRange = null; if (partial.IsMatch(second)) { string newexp = second + ".x"; //treat as X-Range var regex = new Regex(@"\d+(?=\.x)", RegexOptions.IgnoreCase); var r = _RangeHelper(newexp, regex); secondRange = new SemVerRange(SemVer.Min, r.MaximumVersion, true, false); } else { //<=second secondRange = new SemVerRange(SemVer.Min, new SemVer(second), true, true); } return SemVerRange.Intersect(firstRange, secondRange); } } // X-Ranges (1.x, 1.2.x, 1.2.*) else if (Regex.IsMatch(expression, @"^[=vV]*\d+(\.\d+)?\.[x\*]", RegexOptions.IgnoreCase)) { var regex = new Regex(@"\d+(?=\.[x\*])", RegexOptions.IgnoreCase); range = _RangeHelper(expression, regex); } // Partial range (1, 1.2) else if (Regex.IsMatch(expression, @"^[=vV]*\d+(\.\d+)?$", RegexOptions.IgnoreCase)) { string newexp = expression + ".x"; //treat as X-Range var regex = new Regex(@"\d+(?=\.x)", RegexOptions.IgnoreCase); try { range = _RangeHelper(newexp, regex); } catch (ArgumentException) { isError = true; } } //Tilde Ranges else if (expression.StartsWith("~")) { // Tilde pattern 1 (~1, ~1.2) if (Regex.IsMatch(expression, @"^~[=vV]*\d+(\.\d+)?$", RegexOptions.IgnoreCase)) { var newexp = expression.Substring(1) + ".x"; // treat as X-Range var regex = new Regex(@"\d+(?=\.x)", RegexOptions.IgnoreCase); try { range = _RangeHelper(newexp, regex); } catch (ArgumentException) { isError = true; } } // Tilde pattern 2 (~1.2.3, ~1.2.3-beta) else { try { SemVer min = SemVer.Parse(expression.Substring(1)); SemVer max = new SemVer(min.Major, min.Minor + 1, 0); range.MaximumVersion = max; range.MinimumVersion = min; range.IncludeMinimum = true; range.IncludeMaximum = false; range.Expression = $">={min.ToString()} <{max.ToString()}"; } catch (Exception) { isError = true; } } } // Caret Ranges (^1.2, ^0.2.3) else if (expression.StartsWith("^")) { try { var escape = expression.Substring(1).Split('-'); SemVer ver = SemVer.Parse(Regex.Replace(escape[0], @"[xX\*]", "0")); SemVer newver = null; if (ver.Major != 0) { newver = new SemVer(ver.Major + 1); } else if (ver.Minor != 0) { newver = new SemVer(ver.Major, ver.Minor + 1); } else if (ver.Patch != 0) { newver = new SemVer(ver.Major, ver.Minor, ver.Patch + 1); } else if (ver.Revision != 0) { newver = new SemVer(ver.Major, ver.Minor, ver.Patch, ver.Revision + 1); } else { // All zero pattern 1 (^0 or ^0.x) if (Regex.IsMatch(escape[0], @"^[=vV]*0(\.[xX\*])?$")) { newver = new SemVer(1, 0, 0); } // All zero pattern 2 (^0.0 or ^0.0.x) else if (Regex.IsMatch(escape[0], @"^[=vV]*0\.0(\.[xX\*])?$")) { newver = new SemVer(0, 1, 0); } // All zero pattern 3 (^0.0.0 or ^0.0.0.x) else if (Regex.IsMatch(escape[0], @"^[=vV]*0\.0\.0(\.[xX\*])?$")) { newver = new SemVer(0, 0, 1); } else { //parse error throw new Exception(); } } var maxSemVer = newver; var minSemVer = SemVer.Parse(Regex.Replace(escape[0], @"[xX\*]", "0") + "-" + (escape.Length >= 2 ? escape[1] : "")); range.MaximumVersion = maxSemVer; range.MinimumVersion = minSemVer; range.IncludeMinimum = true; range.IncludeMaximum = false; range.Expression = $">={minSemVer.ToString()} <{maxSemVer.ToString()}"; } catch (Exception) { isError = true; } } // Grater equals (>=1.2.0) else if (expression.StartsWith(">=")) { var tmp = expression.Substring(2); if (SemVer.TryParse(tmp, out SemVer tmpVer)) { range.MaximumVersion = SemVer.Max; range.MinimumVersion = tmpVer; range.IncludeMaximum = true; range.IncludeMinimum = true; range.Expression = $">={tmpVer.ToString()}"; } else { isError = true; } } // Grater than (>1.2.0) else if (expression.StartsWith(">")) { var tmp = expression.Substring(1); if (SemVer.TryParse(tmp, out SemVer tmpVer)) { range.MaximumVersion = SemVer.Max; range.MinimumVersion = tmpVer; range.IncludeMaximum = true; range.IncludeMinimum = false; range.Expression = $">{tmpVer.ToString()}"; } else { isError = true; } } // Less equals (<=1.2.0) else if (expression.StartsWith("<=")) { var tmp = expression.Substring(2); if (SemVer.TryParse(tmp, out SemVer tmpVer)) { range.MaximumVersion = tmpVer; range.MinimumVersion = SemVer.Min; range.IncludeMaximum = true; range.IncludeMinimum = true; range.Expression = $"<={tmpVer.ToString()}"; } else { isError = true; } } // Less than (<1.2.0) else if (expression.StartsWith("<")) { var tmp = expression.Substring(1); if (SemVer.TryParse(tmp, out SemVer tmpVer)) { range.MaximumVersion = tmpVer; range.MinimumVersion = SemVer.Min; range.IncludeMaximum = false; range.IncludeMinimum = true; range.Expression = $"<{tmpVer.ToString()}"; } else { isError = true; } } // Strict else { if (SemVer.TryParse(expression, out SemVer tmpVer)) { range.MaximumVersion = tmpVer; range.MinimumVersion = tmpVer; range.IncludeMaximum = true; range.IncludeMinimum = true; range.Expression = tmpVer.ToString(); } else { isError = true; } } if (isError == true) { throw new ArgumentException($"Invalid range expression: \"{expression}\""); } return range; } // _RangeHelper() private static SemVerRange _RangeHelper(string expression, Regex regex) { var escape = expression.Split('-'); var match = regex.Match(escape[0]); var max = regex.Replace(escape[0], (int.Parse(match.Value) + 1).ToString()).Replace('x', '0').Replace('X', '0').Replace('*', '0'); var min = escape[0].Replace('x', '0').Replace('X', '0').Replace('*', '0') + '-' + (escape.Length >= 2 ? escape[1] : ""); try { var ret = new SemVerRange(); var maxSemVer = SemVer.Parse(max); var minSemVer = SemVer.Parse(min); ret.MaximumVersion = maxSemVer; ret.MinimumVersion = minSemVer; ret.IncludeMinimum = true; ret.IncludeMaximum = false; ret.Expression = $">={minSemVer.ToString()} <{maxSemVer.ToString()}"; return ret; } catch (FormatException) { throw new ArgumentException($"Invalid range expression: \"{expression}\""); } } private static bool _IsTupleMatch(SemVer v1, SemVer v2) { return (v1.Major == v2.Major) && (v1.Minor == v2.Minor) && (v1.Patch == v2.Patch) && (v1.Revision == v2.Revision); } /// <summary> /// Test whether the given version satisfies this range /// </summary> /// <param name="version">The version to test</param> /// <param name="range">The range for test</param> /// <returns>Return true if the version satisfies the range</returns> /// <exception cref="System.ArgumentNullException">Thrown when parameter is null</exception> public static bool IsSatisfied(SemVer version, SemVerRange range) { if (range == null || version == null) { throw new ArgumentNullException(); } foreach (var subRange in range.RangeSet) { bool ret = true; bool isTupleMatchedMin = (subRange.MinimumVersion == null) ? false : _IsTupleMatch(subRange.MinimumVersion, version); bool isTupleMatchedMax = (subRange.MaximumVersion == null) ? false : _IsTupleMatch(subRange.MaximumVersion, version); //Lower limit test if (subRange.MinimumVersion != null) { SemVer v = subRange.MinimumVersion; ret &= (subRange.IncludeMinimum ? (version >= v) : (version > v)) && ((v.IsPrerelease() && isTupleMatchedMin) || !version.IsPrerelease() || isTupleMatchedMax); } //Upper limit test if (subRange.MaximumVersion != null) { SemVer v = subRange.MaximumVersion; ret &= (subRange.IncludeMaximum ? (version <= v) : (version < v)) && ((v.IsPrerelease() && isTupleMatchedMax) || !version.IsPrerelease() || isTupleMatchedMin); } if (ret == true) { // short-circuit evaluation return true; } } return false; } /// <summary> /// Test whether the given version satisfies this range /// </summary> /// <param name="version">The version to test</param> /// <returns>Return true if the version satisfies the range</returns> /// <exception cref="System.ArgumentNullException">Thrown when parameter is null</exception> public bool IsSatisfied(SemVer version) => IsSatisfied(version, this); /// <summary> /// Get the highest version in the list that satisfies given range /// </summary> /// <param name="versions">The list of versions to test</param> /// <param name="range">The range for test</param> /// <returns>Returns the highest version in the list that satisfies the range, or null if none of them do</returns> /// <example> /// <code>SemVerRange.MaxSatisfying(new SemVer[]{"1.2.0", "1.2.4", "1.2.99"}, ">1.2.3"); // =>returns "1.2.99"</code> /// </example> /// <exception cref="System.ArgumentNullException">Thrown when parameter is null</exception> public static SemVer MaxSatisfying(SemVer[] versions, SemVerRange range) { if (versions == null || range == null) { throw new ArgumentNullException(); } var dsc = versions.Distinct().OrderByDescending(a => a); foreach (var v in dsc) { if (range.IsSatisfied(v) == true) { return v; } } return null; } /// <summary> /// Get the highest version in the list that satisfies this range /// </summary> /// <param name="versions">The list of versions to test</param> /// <returns>Returns the highest version in the list that satisfies the range, or null if none of them do</returns> /// <exception cref="System.ArgumentNullException">Thrown when parameter is null</exception> public SemVer MaxSatisfying(SemVer[] versions) => MaxSatisfying(versions, this); /// <summary> /// Get the lowest version in the list that satisfies given range /// </summary> /// <param name="versions">The list of versions to test</param> /// <param name="range">The range for test</param> /// <returns>Returns the lowest version in the list that satisfies the range, or null if none of them do</returns> /// <example> /// <code>SemVerRange.MinSatisfying(new Semver[]{"1.2.0", "1.2.4", "1.2.99"}, ">1.2.3"); // =>returns "1.2.4"</code> /// </example> /// <exception cref="System.ArgumentNullException">Thrown when parameter is null</exception> public static SemVer MinSatisfying(SemVer[] versions, SemVerRange range) { if (versions == null || range == null) { throw new ArgumentNullException(); } var dsc = versions.Distinct().OrderBy(a => a); foreach (var v in dsc) { if (range.IsSatisfied(v) == true) { return v; } } return null; } /// <summary> /// Get the lowest version in the list that satisfies this range /// </summary> /// <param name="versions">The list of versions to test</param> /// <returns>Returns the lowest version in the list that satisfies the range, or null if none of them do</returns> public SemVer MinSatisfying(SemVer[] versions) => MinSatisfying(versions, this); /// <summary> /// Get all versions in the list that satisfies the given range /// </summary> /// <param name="versions">The list of versions to test</param> /// <param name="range">The range for test</param> /// <returns>Returns all versions in the list that satisfies the range, or empty array if none of them do</returns> /// <example> /// <code>SemVerRange.Satisfying(new SemVer[]{"1.2.0", "1.2.4", "1.2.99"}, ">1.2.3"); // =>returns {"1.2.4", "1.2.9"}</code> /// </example> public static SemVer[] Satisfying(SemVer[] versions, SemVerRange range) { return versions.Where(v => range.IsSatisfied(v)).ToArray(); } /// <summary> /// Get all versions in the list that satisfies the this range /// </summary> /// <param name="versions">The list of versions to test</param> /// <returns>Returns all versions in the list that satisfies the range, or empty array if none of them do</returns> public SemVer[] Satisfying(SemVer[] versions) => Satisfying(versions, this); /// <summary> /// Calculate the intersection between two ranges /// </summary> /// <param name="range0">The Range to intersect with range1</param> /// <param name="range1">The Range to intersect with range0</param> /// <returns> /// Return the range that intersects between two ranges /// NOTE: If either range0 or range1 is null, it returns the non-null range /// </returns> /// <example> /// <code> /// SemVerRange.Intersect(">1.0.0", "<=2.0.0"); /// // => returns a new range that expressed for ">1.0.0 <=2.0.0" /// </code> /// </example> /// <exception cref="System.ArgumentNullException">Thrown if both range0 and range1 are null</exception> public static SemVerRange Intersect(SemVerRange range0, SemVerRange range1) { if (range0 == null && range1 == null) { throw new ArgumentNullException($"Both {nameof(range0)} and {nameof(range1)} are null"); } if (range0 == null) { return range1; } if (range1 == null) { return range0; } if (range0.RangeSet.Length == 1 && range1.RangeSet.Length == 1) { return _SingleIntersect(range0, range1); } List<SemVerRange> rangeSet = new List<SemVerRange>(); for (int i = 0; i < range0.RangeSet.Length; i++) { for (int j = 0; j < range1.RangeSet.Length; j++) { rangeSet.Add(_SingleIntersect(range0.RangeSet[i], range1.RangeSet[j])); } } return _UnionAll(rangeSet.ToArray()); } /// <summary> /// Calculate the intersection between two ranges /// </summary> /// <param name="range">The Range to intersect with this range</param> /// <returns> /// Return the range that intersects between two ranges /// NOTE: If the input range is null, it returns this range /// </returns> public SemVerRange Intersect(SemVerRange range) => Intersect(this, range); /// <summary> /// Calculate the intersection of multiple ranges /// </summary> /// <param name="ranges">The collection of ranges</param> /// <returns>Return the range that intersects with all ranges</returns> /// <example> /// <code>SemVerRange.IntersectAll(new SemVerRange[]{">1.0.0", "<=2.0.0", "*"});</code> /// </example> /// <exception cref="System.ArgumentNullException">Thrown when input is null</exception> public static SemVerRange IntersectAll(params SemVerRange[] ranges) { if (ranges == null) { throw new ArgumentNullException(); } if (ranges.Length <= 1) { return ranges.FirstOrDefault(); } SemVerRange ret = null; foreach (var r in ranges) { ret = Intersect(ret, r); } return ret; } private static SemVerRange _SingleIntersect(SemVerRange range0, SemVerRange range1) { if (range0 == null && range1 == null) { throw new ArgumentNullException(); } if (range0 == null) { return range1; } if (range1 == null) { return range0; } SemVer newMax = null; SemVer newMin = null; bool newIncludeMax = false; bool newIncludeMin = false; SemVerRange higher, lower; //sort if (range0.MaximumVersion > range1.MaximumVersion) { higher = range0; lower = range1; } else { higher = range1; lower = range0; } // no intersection if (lower.MaximumVersion < higher.MinimumVersion) { return new SemVerRange(); } // determine higher limit if (lower.MaximumVersion == higher.MinimumVersion) { if (lower.IncludeMaximum && higher.IncludeMinimum) { // boundary intersection return new SemVerRange(lower.MaximumVersion.ToString()); } else { return new SemVerRange(); } } else if (lower.MaximumVersion == higher.MaximumVersion) { newMax = lower.MaximumVersion; newIncludeMax = (lower.IncludeMaximum && higher.IncludeMinimum); } else { newMax = lower.MaximumVersion; newIncludeMax = lower.IncludeMaximum; } // determine lower limit if (higher.MinimumVersion > lower.MinimumVersion) { newMin = higher.MinimumVersion; newIncludeMin = higher.IncludeMinimum; } else if (higher.MinimumVersion == lower.MinimumVersion) { newMin = higher.MinimumVersion; newIncludeMin = (higher.IncludeMinimum && lower.IncludeMinimum); } else { newMin = lower.MinimumVersion; newIncludeMin = lower.IncludeMinimum; } return new SemVerRange(newMin, newMax, newIncludeMin, newIncludeMax); } /// <summary> /// Get the range expression string /// </summary> /// <returns>Range expression string</returns> public override string ToString() => this.Expression; private static SemVerRange _Union(SemVerRange range0, SemVerRange range1) { if (range0 == null && range1 == null) { throw new ArgumentNullException(); } if (range0 == null) { return range1; } if (range1 == null) { return range0; } // some special cases if (range0.Expression == ">=0.0.0") { return range0; } if (range1.Expression == ">=0.0.0") { return range1; } if (range0.Expression == "<0.0.0") { return range1; } if (range1.Expression == "<0.0.0") { return range0; } SemVer newMax = null; SemVer newMin = null; bool newIncludeMax = false; bool newIncludeMin = false; SemVerRange higher, lower; //sort if (range0.MaximumVersion > range1.MaximumVersion) { higher = range0; lower = range1; } else { higher = range1; lower = range0; } // no intersection if (lower.MaximumVersion < higher.MinimumVersion) { return _newSemVerRangeSet(lower, higher); } if (lower.MaximumVersion == higher.MinimumVersion) { if (!lower.IncludeMaximum && !higher.IncludeMinimum) { // no intersection return _newSemVerRangeSet(lower, higher); } } newMax = higher.MaximumVersion; newMin = lower.MinimumVersion; newIncludeMax = higher.IncludeMaximum; if (lower.MinimumVersion < higher.MinimumVersion) { newIncludeMin = lower.IncludeMinimum; } else if (lower.MinimumVersion == higher.MinimumVersion) { newIncludeMin = (lower.IncludeMinimum || higher.IncludeMinimum); } else { return higher; } return new SemVerRange(newMin, newMax, newIncludeMin, newIncludeMax); } private static SemVerRange _UnionAll(SemVerRange[] ranges) { if (ranges == null) { throw new ArgumentNullException(); } List<SemVerRange> rangeSet = ranges.ToList(); rangeSet.OrderBy(r => r.MaximumVersion); for (int i = 0; i < (ranges.Length - 1); i++) { var r = _Union(ranges[i], ranges[i + 1]); if (r.RangeSet.Length == 1) { ranges[i] = null; ranges[i + 1] = r; } } rangeSet = ranges.ToList(); rangeSet.RemoveAll(r => r == null); return _newSemVerRangeSet(rangeSet.ToArray()); } } //:------------------------: #endregion SemVerRange //:------------------------: } |