Eli
Eli

Reputation: 337

Storing a version in an integer

So I was thinking of a way to store an entire version string inside an integer, and be able to effectively compare it. I know there are ways to compare versions using methods such as string splitting, but I'm curious to see if something like the following would be possible:

// version =   major << 16 | minor << 8 | revision
int version1 =     1 << 16 |     2 << 8 | 3        // 1.2.3
int version2 =     0 << 16 |    13 << 8 | 2        // 0.13.2
if(version1 > version2) { ... }                    // true

Is this a practical way to store versions and be able to easily compare them? Is there any particular advantage or disadvantage to doing it this way versus splitting and comparing strings?

Upvotes: 0

Views: 1010

Answers (1)

zoravur
zoravur

Reputation: 82

In general, I don't think I'd recommend using an integer to store the version. I find that they're more often represented as strings, and parsed using something like regex. So I'd probably use that, especially if you'd like your code to be easily understood / interoperable with other developers.

That being said, I do see the benefit of using an integer to store a version. It could be made slightly more convenient by wrapping it in a class (this is in C++):

#include <iostream>
#include <iomanip> // std::boolalpha
#include <stdint.h> // uint64_t, uint16_t

// Class representing a version string
class Version {
    const static unsigned int field_size = 16;
    uint64_t version;  // 64 bits => largest field we can support is 16 bits
   public:
    Version(uint16_t major, uint16_t minor, uint16_t revision)
        : version((uint64_t)major << 2 * field_size | (uint64_t)minor << field_size | revision) {}

    // Implicit cast for integer-like operations
    operator uint64_t() const { return version; }

    // Output version string as major.minor.revision
    friend std::ostream& operator<<(std::ostream& os, const Version& v);
};

std::ostream& operator<<(std::ostream& os, const Version& v) {
    const uint16_t mask = (uint16_t)(-1);
    return (
        os << (v >> 2 * Version::field_size & mask) << "." 
           << (v >> Version::field_size & mask) << "." 
           << (v & mask)
    );
}


int main() {
    auto v0 = Version(0, 0, 5);
    auto v1 = Version(1, 2, 3);
    auto v2 = Version(2, 0, 0);
    std::cout << "v0: " << v0 << std::endl;
    std::cout << "v1: " << v1 << std::endl;
    std::cout << "v2: " << v2 << std::endl;
    std::cout << std::boolalpha;
    std::cout << (v1 < v2) << std::endl;
    std::cout << (v1 < v0) << std::endl;
    return 0;
}

/** Output:

$ ./a.out 
v0: 0.0.5
v1: 1.2.3
v2: 2.0.0
true
false

*/

The trick here is to define the conversion to a 64 bit int. Then you get your desired behaviour mostly for free. I'd imagine this approach could be modified for other languages. If the language facilities don't support this kind of conversion, alternative approaches could be defining your own operator overloads, as well as functions for serializing / parsing versions. But then I doubt you'd be gaining much over the struct solution @Zymus mentioned.

Upvotes: 1

Related Questions