From e9ee0179c5a4a749449da9546ba0c19aeb632296 Mon Sep 17 00:00:00 2001
From: Sergio Zharinov <zharinov@users.noreply.github.com>
Date: Fri, 4 Jan 2019 13:11:42 +0400
Subject: [PATCH] feat: maven compare functions (#3020)

Adds comparator function, in readiness for full Ruby versioning support later.
---
 lib/versioning/maven/compare.js | 213 ++++++++++++++++++++++++++++++++
 test/versioning/maven.spec.js   |  72 +++++++++++
 2 files changed, 285 insertions(+)
 create mode 100644 lib/versioning/maven/compare.js
 create mode 100644 test/versioning/maven.spec.js

diff --git a/lib/versioning/maven/compare.js b/lib/versioning/maven/compare.js
new file mode 100644
index 0000000000..865f1830f0
--- /dev/null
+++ b/lib/versioning/maven/compare.js
@@ -0,0 +1,213 @@
+const PREFIX_DOT = 'PREFIX_DOT';
+const PREFIX_HYPHEN = 'PREFIX_HYPHEN';
+
+const TYPE_NUMBER = 'TYPE_NUMBER';
+const TYPE_QUALIFIER = 'TYPE_QUALIFIER';
+
+function iterateChars(str, cb) {
+  let prev = null;
+  let next = null;
+  for (let i = 0; i < str.length; i += 1) {
+    next = str.charAt(i);
+    cb(prev, next);
+    prev = next;
+  }
+  cb(prev, null);
+}
+
+function isDigit(char) {
+  return /^\d$/.test(char);
+}
+
+function isLetter(char) {
+  return /^[a-z]$/i.test(char);
+}
+
+function isTransition(prevChar, nextChar) {
+  return (
+    (isDigit(prevChar) && isLetter(nextChar)) ||
+    (isLetter(prevChar) && isDigit(nextChar))
+  );
+}
+
+function iterateTokens(versionStr, cb) {
+  let currentPrefix = PREFIX_HYPHEN;
+  let currentVal = '';
+
+  function yieldToken(transition = false) {
+    const val = currentVal || '0';
+    if (/^\d+$/.test(val)) {
+      cb({
+        prefix: currentPrefix,
+        type: TYPE_NUMBER,
+        val: parseInt(val, 10),
+        isTransition: transition,
+      });
+    } else {
+      cb({
+        prefix: currentPrefix,
+        type: TYPE_QUALIFIER,
+        val,
+        isTransition: transition,
+      });
+    }
+  }
+
+  iterateChars(versionStr, (prevChar, nextChar) => {
+    if (nextChar === null) {
+      yieldToken();
+    } else if (nextChar === '-') {
+      yieldToken();
+      currentPrefix = PREFIX_HYPHEN;
+      currentVal = '';
+    } else if (nextChar === '.') {
+      yieldToken();
+      currentPrefix = PREFIX_DOT;
+      currentVal = '';
+    } else if (prevChar !== null && isTransition(prevChar, nextChar)) {
+      yieldToken(true);
+      currentPrefix = PREFIX_HYPHEN;
+      currentVal = nextChar;
+    } else {
+      currentVal = currentVal.concat(nextChar);
+    }
+  });
+}
+
+function isNull(token) {
+  const val = token.val;
+  return val === 0 || val === '' || val === 'final' || val === 'ga';
+}
+
+function tokenize(versionStr) {
+  let buf = [];
+  let result = [];
+  iterateTokens(versionStr, token => {
+    if (token.prefix === PREFIX_HYPHEN) {
+      buf = [];
+    }
+    buf.push(token);
+    if (!isNull(token)) {
+      result = result.concat(buf);
+      buf = [];
+    }
+  });
+  return result;
+}
+
+function nullFor(token) {
+  return {
+    prefix: token.prefix,
+    type: token.prefix === PREFIX_DOT ? TYPE_NUMBER : TYPE_QUALIFIER,
+    val: token.prefix === PREFIX_DOT ? 0 : '',
+  };
+}
+
+function commonOrder(token) {
+  if (token.prefix === PREFIX_DOT && token.type === TYPE_QUALIFIER) {
+    return 0;
+  }
+  if (token.prefix === PREFIX_HYPHEN && token.type === TYPE_QUALIFIER) {
+    return 1;
+  }
+  if (token.prefix === PREFIX_HYPHEN && token.type === TYPE_NUMBER) {
+    return 2;
+  }
+  return 3;
+}
+
+function qualifierOrder(token) {
+  const val = token.val;
+  if (val === 'alpha' || (token.isTransition && val === 'a')) {
+    return 1;
+  }
+  if (val === 'beta' || (token.isTransition && val === 'b')) {
+    return 2;
+  }
+  if (val === 'milestone' || (token.isTransition && val === 'm')) {
+    return 3;
+  }
+  if (val === 'rc' || val === 'cr') {
+    return 4;
+  }
+  if (val === 'snapshot') {
+    return 5;
+  }
+  if (val === '' || val === 'final' || val === 'ga') {
+    return 6;
+  }
+  if (val === 'sp') {
+    return 7;
+  }
+  return null;
+}
+
+function qualifierCmp(left, right) {
+  const leftOrder = qualifierOrder(left);
+  const rightOrder = qualifierOrder(right);
+  if (leftOrder && rightOrder) {
+    if (leftOrder === rightOrder) {
+      return 0;
+    }
+    if (leftOrder < rightOrder) {
+      return -1;
+    }
+    return 1;
+  }
+  if (left.val === right.val) {
+    return 0;
+  }
+  if (left.val < right.val) {
+    return -1;
+  }
+  // istanbul ignore next
+  return 1;
+}
+
+function tokenCmp(left, right) {
+  if (left.prefix === right.prefix) {
+    if (left.type === TYPE_NUMBER && right.type === TYPE_NUMBER) {
+      if (left.val === right.val) {
+        return 0;
+      }
+      if (left.val < right.val) {
+        return -1;
+      }
+      return 1;
+    }
+    if (left.type === TYPE_NUMBER) {
+      return 1;
+    }
+    if (right.type === TYPE_NUMBER) {
+      return -1;
+    }
+    return qualifierCmp(left, right);
+  }
+  const leftOrder = commonOrder(left);
+  const rightOrder = commonOrder(right);
+  // istanbul ignore if
+  if (leftOrder === rightOrder) {
+    return 0;
+  }
+  if (leftOrder < rightOrder) {
+    return -1;
+  }
+  return 1;
+}
+
+function compare(left, right) {
+  const leftTokens = tokenize(left.toLowerCase());
+  const rightTokens = tokenize(right.toLowerCase());
+  const length = Math.max(leftTokens.length, rightTokens.length);
+  for (let idx = 0; idx < length; idx += 1) {
+    const leftToken = leftTokens[idx] || nullFor(rightTokens[idx]);
+    const rightToken = rightTokens[idx] || nullFor(leftTokens[idx]);
+    const cmpResult = tokenCmp(leftToken, rightToken);
+    if (cmpResult !== 0) return cmpResult;
+  }
+  return 0;
+}
+
+module.exports = {
+  compare,
+};
diff --git a/test/versioning/maven.spec.js b/test/versioning/maven.spec.js
new file mode 100644
index 0000000000..a3ef2208ec
--- /dev/null
+++ b/test/versioning/maven.spec.js
@@ -0,0 +1,72 @@
+const { compare } = require('../../lib/versioning/maven/compare');
+
+describe('versioning/maven/compare', () => {
+  it('returns equality', () => {
+    expect(compare('1.0.0', '1')).toEqual(0);
+    expect(compare('1-a1', '1-alpha-1')).toEqual(0);
+    expect(compare('1-b1', '1-beta-1')).toEqual(0);
+    expect(compare('1.0.0', '1.ga')).toEqual(0);
+    expect(compare('1-ga', '1.ga')).toEqual(0);
+    expect(compare('1-ga-1', '1-1')).toEqual(0);
+    expect(compare('1.final', '1.0')).toEqual(0);
+    expect(compare('1', '1.0')).toEqual(0);
+    expect(compare('1.', '1-')).toEqual(0);
+    expect(compare('1.0.0-0.0.0', '1-final')).toEqual(0);
+    expect(compare('1-1.foo-bar1baz-.1', '1-1.foo-bar-1-baz-0.1')).toEqual(0);
+    expect(compare('1.0ALPHA1', '1.0-a1')).toEqual(0);
+    expect(compare('1.0Alpha1', '1.0-a1')).toEqual(0);
+    expect(compare('1.0AlphA1', '1.0-a1')).toEqual(0);
+    expect(compare('1.0BETA1', '1.0-b1')).toEqual(0);
+    expect(compare('1.0MILESTONE1', '1.0-m1')).toEqual(0);
+    expect(compare('1.0RC1', '1.0-cr1')).toEqual(0);
+    expect(compare('1.0GA', '1.0')).toEqual(0);
+    expect(compare('1.0FINAL', '1.0')).toEqual(0);
+    expect(compare('1.0-SNAPSHOT', '1-snapshot')).toEqual(0);
+    expect(compare('1.0alpha1', '1.0-a1')).toEqual(0);
+    expect(compare('1.0alpha-1', '1.0-a1')).toEqual(0);
+    expect(compare('1.0beta1', '1.0-b1')).toEqual(0);
+    expect(compare('1.0beta-1', '1.0-b1')).toEqual(0);
+    expect(compare('1.0milestone1', '1.0-m1')).toEqual(0);
+    expect(compare('1.0milestone-1', '1.0-m1')).toEqual(0);
+    expect(compare('1.0rc1', '1.0-cr1')).toEqual(0);
+    expect(compare('1.0rc-1', '1.0-cr1')).toEqual(0);
+    expect(compare('1.0ga', '1.0')).toEqual(0);
+    expect(compare('1-0.ga', '1.0')).toEqual(0);
+    expect(compare('1.0-final', '1.0')).toEqual(0);
+    expect(compare('1-0-ga', '1.0')).toEqual(0);
+    expect(compare('1-0-final', '1-0')).toEqual(0);
+    expect(compare('1-0', '1.0')).toEqual(0);
+  });
+  it('returns less than', () => {
+    expect(compare('1', '1.1')).toEqual(-1);
+    expect(compare('1', '2')).toEqual(-1);
+    expect(compare('1-snapshot', '1')).toEqual(-1);
+    expect(compare('1', '1-sp')).toEqual(-1);
+    expect(compare('1-foo2', '1-foo10')).toEqual(-1);
+    expect(compare('1-m1', '1-milestone-2')).toEqual(-1);
+    expect(compare('1.foo', '1-foo')).toEqual(-1);
+    expect(compare('1-foo', '1-1')).toEqual(-1);
+    expect(compare('1-alpha.1', '1-beta.1')).toEqual(-1);
+    expect(compare('1-1', '1.1')).toEqual(-1);
+    expect(compare('1-ga', '1-ap')).toEqual(-1);
+    expect(compare('1-ga.1', '1-sp.1')).toEqual(-1);
+    expect(compare('1-sp-1', '1-ga-1')).toEqual(-1);
+    expect(compare('1-cr1', '1')).toEqual(-1);
+  });
+  it('returns greater than', () => {
+    expect(compare('1.1', '1')).toEqual(1);
+    expect(compare('2', '1')).toEqual(1);
+    expect(compare('1', '1-snapshot')).toEqual(1);
+    expect(compare('1-sp', '1')).toEqual(1);
+    expect(compare('1-foo10', '1-foo2')).toEqual(1);
+    expect(compare('1-milestone-2', '1-m1')).toEqual(1);
+    expect(compare('1-foo', '1.foo')).toEqual(1);
+    expect(compare('1-1', '1-foo')).toEqual(1);
+    expect(compare('1-beta.1', '1-alpha.1')).toEqual(1);
+    expect(compare('1.1', '1-1')).toEqual(1);
+    expect(compare('1-sp', '1-ga')).toEqual(1);
+    expect(compare('1-sp.1', '1-ga.1')).toEqual(1);
+    expect(compare('1-ga-1', '1-sp-1')).toEqual(1);
+    expect(compare('1', '1-cr1')).toEqual(1);
+  });
+});
-- 
GitLab