Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Challenge accepted:

    from dataclasses import dataclass
    
    @dataclass(frozen=True, order=True)
    class AppVersion:
        major: int = 0
        minor: int = 0
        patch: int = 0
    
        @classmethod
        def from_string(cls, version_string: str):
            return cls(*[int(x) for x in version_string.split(".")])
    
        def __str__(self):
            return f"{self.major}.{self.minor}.{self.patch}"

Before dataclasses you could've used namedtuples, at a loss of attribute typing and default initializer:

    from collections import namedtuple
    
    class AppVersion(namedtuple("AppVersion", "major minor patch")):

        @classmethod
        def from_string(cls, version_string: str):
            parts = [int(x) for x in version_string.split(".")] + [0, 0]
            return cls(*parts[:3])
    
        def __str__(self):
            return f"{self.major}.{self.minor}.{self.patch}"


You could also use a normal class, a direct translation of the Ruby example:

    @functools.total_ordering
    class AppVersion:
      def __init__(self, version_string):
        parts = [int(x) for x in str(version_string).split('.')]
        self.major, self.minor, self.patch = parts[0] or 0, parts[1] or 0, parts[2] or 0

      def __lt__(self, other):
        return [self.major, self.minor, self.patch] < [other.major, other.minor, other.patch]

      def __eq__(self, other):
        return [self.major, self.minor, self.patch] == [other.major, other.minor, other.patch]

      def __str__(self):
        return f'{self.major}.{self.minor}.{self.patch}'


FWIW, these lines are not equivalent:

    @major, @minor, @patch = parts[0] || 0, parts[1] || 0, parts[2] || 0

    self.major, self.minor, self.patch = parts[0] or 0, parts[1] or 0, parts[2] or 0

The Ruby case is intended to handle strings like "1", "1.2", and "1.2.3".

The Python code will throw an IndexError as written. Which is why I did this in the namedtuple example:

    parts = [int(x) for x in version_string.split(".")] + [0, 0]
That ensures you'll have at least three parts so you can then:

    self.major, self.minor, self.patch = parts[:3]


Nice solution with dataclass! And for a complete comparison with the blog you can also use a library to do it for you. It's not quite in the official python distribution but it's maintained by pypa as a dependency of pip so you probably have it installed already.

    >>> from packaging.version import Version
    >>> Version("1.2.3") > Version("1.2.2")
    True
    >>> Version("2.0") > Version("1.2.2")
    True


packaging.version has a somewhat weird (or at least Python-specific) set of rules that don't match the semantics of Ruby's Gem:Version, which will accept basically anything as input.

I'd use `semver` from PyPI and whatever the equivalent Gem is on the Ruby side in most cases.


Not knowing python, I find the data classes example extremely readable. More so than Ruby example.


I write mostly Python these days, but agree with op. The comparables implementation in Ruby seems much nicer to me (maybe because I'm less familiar with it).


It's virtually the same in Python if you wrote it explicitly:

    def <=>(other)
        [major, minor, patch] <=> [other.major, other.minor, other.patch]
    end
vs:

    def __lt__(self, other):
        return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
Then use the `total_ordering` decorator to provide the remaining rich comparison methods.

That said, it's a little annoying Python didn't keep __cmp__ around since there's no direct replacement that's just as succinct and what I did above is a slight fib: you still may need to add __eq__() as well.


I know, but the ability to use symbols to define the comparator is super, super cool, as opposed to the horrendously ugly lt dunder method.


> Then use the `total_ordering` decorator to provide the remaining rich comparison methods.

While we're here, worth highlighting `cmp_to_key` as well for `sorted` etc. calls.

> it's a little annoying Python didn't keep __cmp__ around since there's no direct replacement that's just as succinct

The rationale offered at the time (https://docs.python.org/3/whatsnew/3.0.html) was admittedly weak, but at least this way there isn't confusion over what happens if you try to use both ways (because one of them just isn't a way any more).


However, I think comparing the Ruby example implementation with the "data classes example" is a category error.

The Ruby example should be compared to the implementation of data classes. The Ruby code shows how cleanly the code for parsing, comparing and printing a version string can be. We would need to see the code underlying the data classes implementation to make a meaningful comparison.


It's a little magicky. I guess the "Order=True" is what ensures the order of the parameters in the auto-generated constructor matches the order in which the instance variables are defined?


order: If true (the default is False), __lt__(), __le__(), __gt__(), and __ge__() methods will be generated. These compare the class as if it were a tuple of its fields, in order.

eq: If true (the default), an __eq__() method will be generated. This method compares the class as if it were a tuple of its fields, in order. Both instances in the comparison must be of the identical type.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: