github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/utils/semver/semver.go (about)

     1  package semver
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  )
     9  
    10  type Version struct {
    11  	Major int
    12  	Minor int
    13  	Patch int
    14  }
    15  
    16  func raiseError(err error, context string, values ...any) (*Version, error) {
    17  	if err != nil {
    18  		return nil, errors.New(fmt.Sprintf(context, values...) + ": " + err.Error())
    19  	}
    20  	return nil, fmt.Errorf(context, values...)
    21  }
    22  
    23  // Parse takes a version string with the following pattern: major.minor.patch
    24  // and returns its component parts
    25  func Parse(s string) (*Version, error) {
    26  	if len(s) == 0 {
    27  		return nil, errors.New("empty version string")
    28  	}
    29  
    30  	var err error
    31  	ver := new(Version)
    32  
    33  	split := strings.Split(s, ".")
    34  
    35  	switch len(split) {
    36  	case 3:
    37  		ver.Patch, err = strconv.Atoi(split[2])
    38  		if err != nil {
    39  			return raiseError(err, "patch value is not a number")
    40  		}
    41  		fallthrough
    42  
    43  	case 2:
    44  		ver.Minor, err = strconv.Atoi(split[1])
    45  		if err != nil {
    46  			return raiseError(err, "minor value is not a number")
    47  		}
    48  		fallthrough
    49  
    50  	case 1:
    51  		ver.Major, err = strconv.Atoi(split[0])
    52  		if err != nil {
    53  			return raiseError(err, "major value is not a number")
    54  		}
    55  
    56  	default:
    57  		return raiseError(nil, "too many full stops / period or not a valid version string")
    58  	}
    59  
    60  	return ver, err
    61  }
    62  
    63  func Compare(version string, comparison string) (bool, error) {
    64  	cond, comp := parseComparison(comparison)
    65  	cond = strings.TrimSpace(cond)
    66  	comp = strings.TrimSpace(comp)
    67  
    68  	ver, err := Parse(version)
    69  	if err != nil {
    70  		return false, fmt.Errorf("cannot parse version string: '%s'", err.Error())
    71  	}
    72  
    73  	compV, err := parseCompVersion(comp)
    74  	if err != nil {
    75  		return false, fmt.Errorf("cannot parse comparison string: '%s'", err.Error())
    76  	}
    77  
    78  	switch cond {
    79  	case ">":
    80  		return compare(ver, compV) == greaterThan, nil
    81  	case ">=":
    82  		return compare(ver, compV) != lessThan, nil
    83  	case "", "=", "==":
    84  		return compare(ver, compV) == equalTo, nil
    85  	case "<=":
    86  		return compare(ver, compV) != greaterThan, nil
    87  	case "<":
    88  		return compare(ver, compV) == lessThan, nil
    89  	default:
    90  		return false, fmt.Errorf("unknown comparison token '%s'", cond)
    91  	}
    92  }
    93  
    94  func parseComparison(comparison string) (string, string) {
    95  	for i := range comparison {
    96  		switch {
    97  		case comparison[i] == ' ':
    98  			continue
    99  		case comparison[i] <= '9' && '0' <= comparison[i]:
   100  			return comparison[:i], comparison[i:]
   101  		}
   102  	}
   103  	return "", ""
   104  }
   105  
   106  func parseCompVersion(s string) (*Version, error) {
   107  	if len(s) == 0 {
   108  		return nil, errors.New("empty version string")
   109  	}
   110  
   111  	var err error
   112  	ver := new(Version)
   113  
   114  	split := strings.Split(s, ".")
   115  
   116  	switch len(split) {
   117  	case 3:
   118  		ver.Patch, err = strconv.Atoi(split[2])
   119  		if err != nil {
   120  			return raiseError(err, "patch value is not a number")
   121  		}
   122  		ver.Patch += 2
   123  		fallthrough
   124  
   125  	case 2:
   126  		ver.Patch--
   127  		ver.Minor, err = strconv.Atoi(split[1])
   128  		if err != nil {
   129  			return raiseError(err, "minor value is not a number")
   130  		}
   131  		ver.Minor++
   132  		fallthrough
   133  
   134  	case 1:
   135  		ver.Major, err = strconv.Atoi(split[0])
   136  		if err != nil {
   137  			return raiseError(err, "major value is not a number")
   138  		}
   139  		ver.Patch--
   140  		ver.Minor--
   141  
   142  	default:
   143  		return raiseError(nil, "too many full stops / period or not a valid version string")
   144  	}
   145  
   146  	return ver, err
   147  }
   148  
   149  const (
   150  	lessThan    = -1
   151  	equalTo     = 0
   152  	greaterThan = 1
   153  )
   154  
   155  func compare(version *Version, comparison *Version) int {
   156  	switch {
   157  	case version.Major > comparison.Major:
   158  		return greaterThan
   159  	case version.Major < comparison.Major:
   160  		return lessThan
   161  
   162  	case comparison.Minor < 0:
   163  		return equalTo
   164  
   165  	case version.Minor > comparison.Minor:
   166  		return greaterThan
   167  	case version.Minor < comparison.Minor:
   168  		return lessThan
   169  
   170  	case comparison.Patch < 0:
   171  		return equalTo
   172  
   173  	case version.Patch > comparison.Patch:
   174  		return greaterThan
   175  	case version.Patch < comparison.Patch:
   176  		return lessThan
   177  
   178  	default:
   179  		return equalTo
   180  	}
   181  }