Metadata-Version: 2.4
Name: hank-format
Version: 0.2.0
Summary: Shared string formatting functions for standardizing values across the Hank AI stack.
Author-email: Sam Hartzog <sam.hartzog@hank.ai>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
License-File: LICENSE
Requires-Dist: python-dateutil>=2.8,<3
Requires-Dist: nameparser>=1.3,<2
Project-URL: Home, https://github.com/hank-ai/hank-format
Project-URL: Issues, https://github.com/hank-ai/hank-format/issues

# hank-format

Shared string formatting functions for standardizing names, dates, and times
across the Hank AI stack.

```bash
pip install hank-format
```

## Usage

```python
from hank_format import as_name, as_date, as_date_str, as_datetime_str, as_time_str

as_name("Primary: John Doe")                       # 'DOE, JOHN'
as_name("Steven Hardman, APRN - CRNA")             # 'HARDMAN, STEVEN, APRN, CRNA'
as_name(last="Doe", first="John", suffix="Jr, MD") # 'DOE, JOHN, JR, MD'
as_name(provider_dict={"fn": "John", "ln": "Doe", "tit": "MD"})  # 'DOE, JOHN, MD'
as_date("12/31/2020 (age 30 yrs)")                 # datetime.date(2020, 12, 31)
as_date_str("01012024")                            # '2024-01-01'
as_datetime_str("2020-12-31T23:59")                # '2020-12-31 23:59:00'
as_datetime_str("23:59", precision="minute", default_date="2020-12-31")  # '2020-12-31 23:59'
as_time_str("11:30 pm")                            # '23:30'
```

## API

| Function | Returns | Notes |
| --- | --- | --- |
| `as_name(value)` | `str` | `LAST, FIRST MIDDLE, SUFFIX`, upper case. Strips `Label:` prefixes, `[notes]`, `(notes)`, `*`, and text after `;`. |
| `as_name(*, last, first=None, middle=None, suffix=None)` | `str` | Same output, built from parts. `suffix` may hold several credentials (`"Jr, MD"`). |
| `as_name(*, provider_dict)` | `str` | Same output, built from a dumped hank-medicalnotes `Provider` (`ln`, `fn`, `m`, `sfx`, `tit`). `tit` is appended as a credential unless it is `OTHER` or `UNKNOWN`; other keys are ignored. `ProviderDict` is exported for typing. |
| `is_date(value)` | `bool` | True when a year, month, and day are present. |
| `has_time(value)` | `bool` | True when an hour and minute are present. |
| `as_date(value)` | `date \| None` | Calendar date only. |
| `as_date_str(value)` | `str` | `YYYY-MM-DD` or `""`. |
| `as_datetime(value, *, default_date=None)` | `datetime \| None` | Midnight when no time is present. Time-only input needs `default_date`. |
| `as_datetime_str(value, *, precision="second", default_date=None)` | `str` | `YYYY-MM-DD HH:MM:SS`, or `YYYY-MM-DD HH:MM` with `precision="minute"`. |
| `as_time(value)` | `time \| None` | Clock time only. |
| `as_time_str(value, *, precision="minute")` | `str` | `HH:MM`, or `HH:MM:SS` with `precision="second"`. |

Conventions:

- Inputs may be strings, `date`, `datetime`, `time`, or `None`. Other types raise `TypeError`.
- `as_name` takes exactly one input shape (free text, parts, or `provider_dict`); omitting or mixing them raises `TypeError`, and a supplied `None` means empty. Every shape runs through the same parser, so the output is always a fixed point of `as_name`.
- Object variants return `None` and string variants return `""` when no usable value exists.
- Returned values are timezone-naive and have no microseconds. `precision` truncates, never rounds.
- Any parsed date more than two days in the future rolls back 100 years, including four-digit years and year-less `MM/DD` (`12/31/45` is 1945; `12/26` without a time is 1926 when today is 2026-10-08). This is a two-digit-year heuristic inherited from pdf-extractor; strip or validate far-future dates before calling if you need them intact.
- Year-less `MM/DD HH:MM` strings take the current year, or the previous year if that would be in the future.

## Migrating from pdf-extractor `utils`

| Legacy | hank-format |
| --- | --- |
| `as_name(s)` | `as_name(s)` (lists and `vStr` handling stay in pdf-extractor) |
| `as_date(v, date_only=True, as_dt=True)` | `as_date(v)` (returns `date`) |
| `as_date(v, date_only=True)` | `as_date_str(v)` |
| `as_date(v, as_dt=True)` | `as_datetime(v)` |
| `as_date(v)` | `as_datetime_str(v)` |
| `as_time(v, with_date=False)` | `as_time_str(v)` |
| `as_time(v, strict=True)` | `as_datetime_str(v, precision="minute") if has_time(v) else ""` |
| `as_time(v, default_date=d)` | `as_datetime_str(v, precision="minute", default_date=d) if has_time(v) else ""` |
| `as_time(v)` | `as_datetime_str(v, precision="minute") if is_date(v) and has_time(v) else as_time_str(v)` |

Legacy `as_time` returned `""` for anything without a time; the new datetime
functions format a date-only value as midnight, hence the `has_time` guards.

## Development

Requires [uv](https://docs.astral.sh/uv/).

```bash
uv sync                      # create .venv with Python 3.12 and the dev group
uv run pytest                # tests with 100% coverage enforced
uv run ruff check . && uv run ruff format --check .
uv run pyright
uv build                     # sdist + wheel into dist/
```

## Releasing

1. Open a PR against `main` and add exactly one label: `release:patch`, `release:minor`, or `release:major`.
2. On merge, the `Release` workflow bumps `__version__` in `hank_format/__init__.py`, runs the tests, builds, commits `Release vX.Y.Z`, tags it, and pushes. A separate `publish` job then uploads the built artifacts to PyPI.
3. If only the `publish` job fails, fix the cause and re-run that failed job. Do not re-run the whole workflow: the version is already bumped and tagged, so a full re-run would bump it again.

