package versionutil import ( "fmt" "regexp" "strconv" "strings" "github.com/coreos/go-semver/semver" ) // ByVersion implements sorting versions (by component) using semver if applicable type ByVersion []string func (s ByVersion) Len() int { return len(s) } func (s ByVersion) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ByVersion) Less(i, j int) bool { //we need to compare string by ignoring it's case return Compare(s[i], s[j], true) < 0 } // Compare returns 0 if two versions are equal a negative number if this < other and a positive number if this > other // If this and other are both compliant with semver, then semver sorting rules are used // Otherwise the versions are compared component-by-component, numerically if both are numeric // If !strictSort insignificant trailing components are ignored (1.0.0.0 == 1) and the alpha comparison is case-insensitive func Compare(this string, other string, strictSort bool) int { // If both versions are compliant with SemVer, use the SemVer comparison rules thisSemVer, thisSemErr := semver.NewVersion(this) otherSemVer, otherSemErr := semver.NewVersion(other) if thisSemErr == nil && otherSemErr == nil { return thisSemVer.Compare(*otherSemVer) } thisVersion := this otherVersion := other if !strictSort { // Unless we need a strict ordering, trailing 0 components of version should be ignored thisVersion = normalizeForCompare(thisVersion) otherVersion = normalizeForCompare(otherVersion) } thisComponents := strings.Split(thisVersion, ".") otherComponents := strings.Split(otherVersion, ".") for i := 0; i < len(thisComponents) && i < len(otherComponents); i++ { thisNum, errThis := strconv.Atoi(thisComponents[i]) otherNum, errOther := strconv.Atoi(otherComponents[i]) isNumeric := errThis == nil && errOther == nil if isNumeric { // If we can compare numbers, compare numbers if thisNum < otherNum { return -1 } else if thisNum > otherNum { return 1 } } else { // If either component is not numeric, compare them as text if thisComponents[i] < otherComponents[i] { return -1 } else if thisComponents[i] > otherComponents[i] { return 1 } } } // If all components are equal until we run out of components then the // version with fewer components is the lesser version return len(thisComponents) - len(otherComponents) } // normalizeForCompare removes components at the end that are numerically equal to 0 func normalizeForCompare(version string) string { if len(version) == 0 { return version } lenSignificant := len(version) for i := len(version) - 1; i >= 0; i-- { if version[i] != '0' && version[i] != '.' { break } if version[i] == '.' { lenSignificant = i } if i == 0 { lenSignificant = 0 } } return version[0:lenSignificant] } func IsValidVersion(version string) bool { _, err := versionOrdinal(strings.TrimSpace(version)) return err == nil } // VersionCompare compares two version strings func VersionCompare(versionl string, versionr string) (result int, err error) { if versionl, err = versionOrdinal(strings.TrimSpace(versionl)); err != nil { return 0, err } if versionr, err = versionOrdinal(strings.TrimSpace(versionr)); err != nil { return 0, err } if versionl < versionr { return -1, nil } else if versionl > versionr { return 1, nil } else { return 0, nil } } func versionOrdinal(version string) (string, error) { // validate if string is a valid version string if matched, err := regexp.MatchString("\\d+(\\.\\d+)?", version); matched == false || err != nil { return "", fmt.Errorf("Invalid version string %v", version) } // ISO/IEC 14651:2011 const maxByte = 1<<8 - 1 vo := make([]byte, 0, len(version)+8) j := -1 for i := 0; i < len(version); i++ { b := version[i] if '0' > b || b > '9' { vo = append(vo, b) j = -1 continue } if j == -1 { vo = append(vo, 0x00) j = len(vo) - 1 } if vo[j] == 1 && vo[j+1] == '0' { vo[j+1] = b continue } if vo[j]+1 > maxByte { return "", fmt.Errorf("VersionOrdinal: invalid version") } vo = append(vo, b) vo[j]++ } return string(vo), nil }