Skip to content

Commit

Permalink
Fix negative duration being wrongly displayed (#4)
Browse files Browse the repository at this point in the history
* Fix negative duration being wrongly displayed

* Move from setup.cfg to pure pyproject.toml and add __main__ for -m execution

* Adds Python 3.11 to CI

* Add MacOS to CI
  • Loading branch information
philippewarren authored Dec 4, 2022
1 parent e4c3bfe commit ebbbb93
Show file tree
Hide file tree
Showing 15 changed files with 391 additions and 70 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v3

- name: Installing Python 3.10
- name: Installing Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"

- name: Installing tools and dependencies
run: |
Expand All @@ -43,8 +43,8 @@ jobs:
needs: lint
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-20.04, windows-latest]
python-version: ["3.9", "3.10", "pypy3.9"]
os: [ubuntu-22.04, ubuntu-20.04, windows-latest, macos-latest]
python-version: ["3.9", "3.10", "3.11", "pypy3.9"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
Expand Down Expand Up @@ -79,10 +79,10 @@ jobs:
with:
fetch_depth: 0

- name: Installing Python 3.10
- name: Installing Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"

- name: Installing tools and dependencies
run: |
Expand Down
17 changes: 17 additions & 0 deletions calct/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# calct: Easily do calculations on hours and minutes using the command line
# Copyright (C) 2022 Philippe Warren
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.


from calct.__version__ import __version__
from calct.duration import Duration
from calct.main import __author__, __license__, __year__, run_loop, run_once
Expand Down
21 changes: 21 additions & 0 deletions calct/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# calct: Easily do calculations on hours and minutes using the command line
# Copyright (C) 2022 Philippe Warren
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.


from calct.main import main

if __name__ == "__main__":
main()
46 changes: 46 additions & 0 deletions calct/_divmod.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# calct: Easily do calculations on hours and minutes using the command line
# Copyright (C) 2022 Philippe Warren
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from enum import IntEnum
from typing import Tuple

from calct._common import Number


class Sign(IntEnum):
"""Sign of a number."""

NULL = 0
POSITIVE = 1
NEGATIVE = -1

@property
def char(self):
return "-" if self == Sign.NEGATIVE else ""


def sign(number: Number) -> Sign:
if number == 0:
return Sign.NULL
return Sign(int(number) // int(abs(number)))


def duration_friendly_divmod(numerator: int, denominator: int) -> Tuple[Sign, int, int]:
"""Return a tuple (sign, quotient, remainder) such that
numerator = (sign * quotient) * denominator + (sign * remainder).
"""

return (sign(numerator * denominator), *divmod(abs(numerator), abs(denominator)))
44 changes: 25 additions & 19 deletions calct/duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
DEFAULT_MINUTE_SEPARATOR,
Number,
)
from calct._divmod import duration_friendly_divmod
from calct._duration_parser import compile_matcher, parse_duration


Expand Down Expand Up @@ -60,16 +61,27 @@ def del_string_hour_minute_separator(cls) -> None:
cls.str_hour_sep = DEFAULT_HOUR_SEPARATOR[0]

def __init__(self, hours: Number = 0, minutes: int = 0) -> None:
self.minutes: int = int(hours * 60) + minutes
self.total_minutes: int = int(hours * 60) + minutes

@property
def hours(self) -> int:
"""The `hours` part of the duration, truncated"""
return divmod(self.minutes, 60)[0]
sign, hours, _ = duration_friendly_divmod(self.total_minutes, 60)
return sign * hours

@hours.setter
def hours(self, new_hours: Number) -> None:
self.minutes = int(new_hours * 60)
self.total_minutes = int(new_hours * 60)

@property
def minutes(self) -> int:
"""The `minutes` part of the duration, truncated"""
sign, _, minutes = duration_friendly_divmod(self.total_minutes, 60)
return sign * minutes

@minutes.setter
def minutes(self, new_minutes: int) -> None:
self.total_minutes = self.hours * 60 + new_minutes

@staticmethod
def from_timedelta(time_delta: timedelta) -> Duration:
Expand Down Expand Up @@ -111,53 +123,47 @@ def parse(cls, time_str: str) -> Duration:
pass
raise ValueError(f"Invalid time: {time_str}")

@property
def hours_minutes(self) -> tuple[int, int]:
"""The `hours` and `minutes` parts of the duration."""
return divmod(self.minutes, 60)

def __str__(self) -> str:
hours, minutes = self.hours_minutes
return f"{hours:.0f}{self.str_hour_sep}{minutes:02.0f}"
sign, hours, minutes = duration_friendly_divmod(self.total_minutes, 60)
return f"{'-' if sign == -1 else ''}{hours}{self.str_hour_sep}{minutes:02}"

def __repr__(self) -> str:
hours, minutes = self.hours_minutes
return f"Duration(hours={hours}, minutes={minutes})"
return f"Duration(hours={self.hours}, minutes={self.minutes})"

def __eq__(self, other: object) -> bool:
if not isinstance(other, Duration): # type: ignore
raise TypeError(f"unsupported operand type(s) for ==: '{type(self)}' and '{type(other)}'")
return self.minutes == other.minutes
return self.total_minutes == other.total_minutes

def __lt__(self, other: Duration) -> bool:
if not isinstance(other, Duration): # type: ignore
raise TypeError(f"unsupported operand type(s) for <: '{type(self)}' and '{type(other)}'")
return self.minutes < other.minutes
return self.total_minutes < other.total_minutes

def __add__(self, other: Duration) -> Duration:
if not isinstance(other, Duration): # type: ignore
raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'")
return Duration(minutes=self.minutes + other.minutes)
return Duration(minutes=self.total_minutes + other.total_minutes)

def __sub__(self, other: Duration) -> Duration:
if not isinstance(other, Duration): # type: ignore
raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'")
return Duration(minutes=self.minutes - other.minutes)
return Duration(minutes=self.total_minutes - other.total_minutes)

def __mul__(self, other: Number) -> Duration:
if not isinstance(other, (int, float)): # type: ignore
raise TypeError(f"unsupported operand type(s) for *: '{type(self)}' and '{type(other)}'")
return Duration(minutes=int(self.minutes * other))
return Duration(minutes=int(self.total_minutes * other))

def __rmul__(self, other: Number) -> Duration:
return self.__mul__(other)

def __truediv__(self, other: Number) -> Duration:
if not isinstance(other, (int, float)): # type: ignore
raise TypeError(f"unsupported operand type(s) for /: '{type(self)}' and '{type(other)}'")
return Duration(minutes=int(self.minutes / other))
return Duration(minutes=int(self.total_minutes / other))

@property
def as_timedelta(self) -> timedelta:
"""Return the duration as a timedelta."""
return timedelta(minutes=self.minutes)
return timedelta(minutes=self.total_minutes)
16 changes: 16 additions & 0 deletions calct/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ def get_licence_str() -> str:
raise NotImplementedError()


def get_version_str() -> str:
"""Return the version string for the program"""
return f"{__version__}"


def run_once(time_expr_list: list[str]) -> None:
"""Run the computation on an expression once"""

Expand Down Expand Up @@ -173,6 +178,7 @@ class Args(argparse.Namespace):
interactive: bool = False
help: bool = False
licence: bool = False
version: bool = False
separator: str = "h"


Expand Down Expand Up @@ -203,6 +209,13 @@ def main():
help="Show this help message and exit",
default=False,
)
parser.add_argument(
"-v",
"--version",
action="store_true",
help="Show the version and exit",
default=False,
)
parser.add_argument(
"--license",
action="store_true",
Expand Down Expand Up @@ -236,6 +249,9 @@ def main():
elif args.licence:
print(get_licence_str())
sys.exit()
elif args.version:
print(get_version_str())
sys.exit()
elif args.interactive:
run_loop()
elif len(remaining_args) > 0:
Expand Down
52 changes: 52 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,55 @@
requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=7.0.5"]
build-backend = "setuptools.build_meta"

[project]
name = "calct"
authors = [
{name = "Philippe Warren", email = "[email protected]"},
]
description = "Easily do calculations on hours and minutes using the command line"
readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Natural Language :: English",
"Natural Language :: French",
"Topic :: Utilities",
"Typing :: Typed",
]
dynamic = ["version"]

[project.optional-dependencies]
dev = [
"ipython>=8.4.0",
"black>=22.6.0",
"isort>=5.10.1",
"mypy>=0.971",
"pylint>=2.15.0",
"flake8>=5.0.4",
"Flake8-pyproject>=1.2.1",
]
test = [
"pytest>=7.1.2",
"pytest-cov>=3.0.0",
]

[project.urls]
homepage = "https://github.com/philippewarren/calct"
repository = "https://github.com/philippewarren/calct"
changelog = "https://github.com/philippewarren/calct/releases"

[project.scripts]
calct = "calct.main:main"

[tool.setuptools]
packages = ["calct"]
dynamic = {version = {attr = "calct.__version__"}}

[tool.pytest.ini_options]
required_plugins = "pytest-cov>=3.0.0"
addopts = "--cov=calct --cov-branch --cov-report=html:htmlcov_calct"
Expand Down Expand Up @@ -30,3 +79,6 @@ disable = [
]
enable = ["useless-suppression"]
max-line-length = 120

[tool.flake8]
max_line_length = 120
43 changes: 0 additions & 43 deletions setup.cfg

This file was deleted.

Loading

0 comments on commit ebbbb93

Please sign in to comment.