Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/wheel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Build wheel
run: uv build --wheel --out-dir wheelhouse
- name: Build package
run: make wheel OUT_DIR=wheelhouse

# https://github.com/actions/upload-artifact
- uses: actions/upload-artifact@v6
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ build
cruft
dist
docs
man/*.1
src/*.egg-info
venv
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
OUT_DIR ?= dist

.PHONY: all
all: wheel

.PHONY: test
test:
@echo "==> $@"
uv run pytest

.PHONY: clean
clean:
@echo "==> $@"
rm -rf dist/ build/ *.egg-info man/datetimecalc.1

.PHONY: wheel
wheel: manpages
@echo "==> $@"
uv build --wheel --out-dir $(OUT_DIR)

.PHONY: manpages
manpages: man/datetimecalc.1

man/datetimecalc.1: man/extra-sections.man src/datetimecalc/__main__.py
@echo "==> $@"
uv run argparse-manpage \
--module datetimecalc.__main__ \
--function get_parser \
--project-name datetimecalc \
--description "parse and compute with natural language datetime expressions" \
--author "Backplane <actualben@users.noreply.github.com>" \
--url "https://github.com/backplane/datetimecalc" \
--include "$<" \
--output "$@"
108 changes: 108 additions & 0 deletions man/extra-sections.man
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
[EXPRESSIONS]
Expressions consist of temporal values (datetimes, timedeltas, or timezones)
combined with operators.

.SS Operators
.TP
.B +
Add a timedelta to a datetime, or add two timedeltas.
.TP
.B \-
Subtract a timedelta from a datetime, subtract two datetimes to get a
timedelta, or subtract two timedeltas.
.TP
.B @
Convert a datetime to a different timezone.
.TP
.BR < ", " <= ", " > ", " >= ", " == ", " !=
Compare two datetimes or two timedeltas.

.SS Datetime Formats
Datetimes can be specified in various formats:
.IP \(bu 2
ISO format: 2024-01-15, 2024-01-15 14:30
.IP \(bu 2
Natural language: tomorrow, next Friday, yesterday at 3pm
.IP \(bu 2
With timezone: 2024-01-15 14:30 UTC, tomorrow America/New_York
.IP \(bu 2
With UTC offset: 2024-01-15 +05:30

.SS Timedelta Formats
Durations support various units and formats:
.IP \(bu 2
Full words: 1 day, 2 hours, 30 minutes
.IP \(bu 2
Abbreviations: 1d, 2h, 30m, 15s
.IP \(bu 2
Combined: 1 day 2 hours 30 minutes, 1d2h30m
.IP \(bu 2
Fractional: 1.5 hours, 2.5 days
.PP
Supported units: years (y), months (mo), weeks (w), days (d), hours (h),
minutes (m), seconds (s), milliseconds (ms), microseconds (us).
.PP
.B Note:
Years and months use fixed approximations (1 year = 365 days,
1 month = 30 days).

.SS Timezone Formats
Timezones can be specified as:
.IP \(bu 2
IANA names: America/New_York, Europe/London, Asia/Tokyo
.IP \(bu 2
Abbreviations: UTC, EST, PST (where unambiguous)
.IP \(bu 2
UTC offsets: +05:30, -08:00

[EXAMPLES]
.TP
Add one week to a date:
.B datetimecalc \(dq2024-01-01 + 1 week\(dq
.TP
Calculate days until a date:
.B datetimecalc \(dq2025-01-01 - now\(dq
.TP
Convert timezone:
.B datetimecalc \(dq2024-01-01 12:00 America/New_York @ UTC\(dq
.TP
Compare durations:
.B datetimecalc \(dq1 day == 24 hours\(dq
.TP
Natural language with multiple arguments (no quotes needed):
.B datetimecalc tomorrow at 3pm + 2 hours
.TP
Get Python repr output:
.B datetimecalc \-\-repr \(dq2025-01-01 - 2024-01-01\(dq

[OUTPUT]
By default, datetimes are formatted in ISO 8601 format and timedeltas
are formatted in a human-readable localized form based on system locale.
.PP
With
.BR \-\-repr ,
output uses Python's repr() format (e.g., datetime.timedelta(days=7)).

[LOCALIZATION]
Timedelta output is automatically localized based on the system locale.
Supported languages: English, Spanish, Chinese, Hindi, Portuguese,
Bengali, Russian, Japanese, Vietnamese, Turkish, Marathi.

[EXIT STATUS]
.TP
.B 0
Success.
.TP
.B 1
Error parsing expression or invalid input.

[SEE ALSO]
.BR date (1),
.BR python3 (1)

[BUGS]
Only single binary operations are supported. Chained expressions like
"a + b + c" do not work; use separate invocations or parentheses in a shell.

[COPYRIGHT]
Copyright \(co 2024-2026 Backplane. Licensed under the Apache License 2.0.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ build-backend = "uv_build"
dev = [
"pytest>=8.4.2",
"pdoc3>=0.11.1",
"argparse-manpage>=4.7",
]
24 changes: 20 additions & 4 deletions src/datetimecalc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,29 @@
import argparse
import logging
import sys
from importlib.metadata import version

from .functions import format_temporal_object, parse_temporal_expr


def main() -> int:
def get_parser() -> argparse.ArgumentParser:
"""
entrypoint for direct execution; returns an integer suitable for use with
sys.exit
Build and return the argument parser for datetimecalc.

This function is also used by argparse-manpage to generate the manpage.
"""
argp = argparse.ArgumentParser(
prog=__package__,
prog="datetimecalc",
description=(
"program which parses natural language datetime and timedelta expressions"
),
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
argp.add_argument(
"--version",
action="version",
version=f"%(prog)s {version('datetimecalc')}",
)
argp.add_argument(
"--debug",
action="store_true",
Expand All @@ -45,6 +52,15 @@ def main() -> int:
nargs="+",
help="a natural language date and time operation expression",
)
return argp


def main() -> int:
"""
entrypoint for direct execution; returns an integer suitable for use with
sys.exit
"""
argp = get_parser()
args = argp.parse_args()

logging.basicConfig(
Expand Down
8 changes: 8 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.