calckit-kabanda 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ release:
8
+ types: [published]
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ id-token: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+ - name: Build
21
+ run: |
22
+ python -m pip install --upgrade pip build
23
+ python -m build
24
+ - name: Verify metadata
25
+ run: python -m pip install twine && python -m twine check dist/*
26
+ - name: Publish
27
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,30 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install
20
+ run: |
21
+ python -m pip install --upgrade pip
22
+ pip install -e .
23
+ pip install pytest
24
+ - name: Run tests
25
+ run: pytest -q
26
+ - name: Build check
27
+ run: |
28
+ pip install build twine
29
+ python -m build
30
+ python -m twine check dist/*
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .coverage
11
+ htmlcov/
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0]
9
+
10
+ ### Added
11
+
12
+ - Arithmetic: `add`, `subtract`, `multiply`, `divide`, `integer_divide`,
13
+ `power`, `modulus`, `absolute`, `root`, `square_root`, `logarithm`,
14
+ `factorial`
15
+ - Statistics: `total`, `mean`, `average`, `median`, `mode`, `minimum`,
16
+ `maximum`, `value_range`, `variance`, `standard_deviation`,
17
+ `geometric_mean`, `harmonic_mean`
18
+ - Errors: `CalcError`, `DivisionByZeroError`, `EmptySequenceError`
19
+ - `calckit-kabanda` command line interface, also runnable as
20
+ `python -m calc_toolkit`
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kabanda-jordan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.5
2
+ Name: calckit-kabanda
3
+ Version: 0.1.0
4
+ Summary: Arithmetic and statistics helpers: add, subtract, multiply, divide, mean, median, mode and more.
5
+ Project-URL: Homepage, https://github.com/kabanda-jordan/calckit-kabanda
6
+ Project-URL: Repository, https://github.com/kabanda-jordan/calckit-kabanda
7
+ Project-URL: Issues, https://github.com/kabanda-jordan/calckit-kabanda/issues
8
+ Project-URL: Changelog, https://github.com/kabanda-jordan/calckit-kabanda/blob/main/CHANGELOG.md
9
+ Author: kabanda-jordan
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: arithmetic,average,calculator,math,mean,statistics
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Education
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+
30
+ # calckit-kabanda
31
+
32
+ Arithmetic and statistics helpers for Python, plus a `calckit-kabanda` command line tool.
33
+
34
+ ## Install
35
+
36
+ From GitHub (no PyPI account needed, works immediately):
37
+
38
+ ```powershell
39
+ pip install git+https://github.com/kabanda-jordan/calckit-kabanda.git
40
+ ```
41
+
42
+ From PyPI, once published:
43
+
44
+ ```powershell
45
+ pip install calckit-kabanda
46
+ ```
47
+
48
+ Or from a checkout:
49
+
50
+ ```powershell
51
+ pip install -e .
52
+ ```
53
+
54
+ The distribution is named `calckit-kabanda` on PyPI, but the import name stays
55
+ `calc_toolkit`, so code reads:
56
+
57
+ ```python
58
+ from calc_toolkit import add, subtract, multiply, divide, mean, median
59
+ ```
60
+
61
+ ## Library use
62
+
63
+ ```python
64
+ add(1, 2, 3) # 6
65
+ subtract(10, 3, 2) # 5
66
+ multiply(2, 3, 4) # 24
67
+ divide(10, 4) # 2.5
68
+
69
+ mean(2, 4, 6) # 4.0 (same as average)
70
+ median(1, 3, 2) # 2.0
71
+ mode(1, 2, 2, 3) # 2.0
72
+ ```
73
+
74
+ ### Arithmetic
75
+
76
+ | Function | Example | Result |
77
+ | --- | --- | --- |
78
+ | `add(*values)` | `add(1, 2, 3)` | `6` |
79
+ | `subtract(a, *rest)` | `subtract(10, 3)` | `7` |
80
+ | `multiply(*values)` | `multiply(2, 3, 4)` | `24` |
81
+ | `divide(a, b)` | `divide(10, 4)` | `2.5` |
82
+ | `integer_divide(a, b)` | `integer_divide(10, 3)` | `3` |
83
+ | `power(base, exp)` | `power(2, 10)` | `1024` |
84
+ | `modulus(a, b)` | `modulus(10, 3)` | `1` |
85
+ | `absolute(x)` | `absolute(-5)` | `5` |
86
+ | `root(x, degree=2)` | `root(27, 3)` | `3.0` |
87
+ | `square_root(x)` | `square_root(9)` | `3.0` |
88
+ | `logarithm(x, base=e)` | `logarithm(8, 2)` | `3.0` |
89
+ | `factorial(n)` | `factorial(5)` | `120` |
90
+
91
+ ### Statistics
92
+
93
+ All statistics accept a variable number of values, so both
94
+ `mean([2, 4, 6])` and `mean(2, 4, 6)` work.
95
+
96
+ | Function | Example | Result |
97
+ | --- | --- | --- |
98
+ | `total(*values)` | `total(1, 2, 3)` | `6.0` |
99
+ | `mean(*values)` / `average` | `mean(2, 4, 6)` | `4.0` |
100
+ | `median(*values)` | `median(4, 1, 3, 2)` | `2.5` |
101
+ | `mode(*values)` | `mode(1, 2, 2)` | `2.0` |
102
+ | `minimum(*values)` | `min(3, 1, 2)` | `1.0` |
103
+ | `maximum(*values)` | `max(3, 1, 2)` | `3.0` |
104
+ | `value_range(*values)` | `range(3, 1, 2)` | `2.0` |
105
+ | `variance(*values, sample=False)` | `variance(2, 4, 6)` | `2.666…` |
106
+ | `standard_deviation(*values)` | `stdev(2, 4, 6)` | `1.632…` |
107
+ | `geometric_mean(*values)` | `geometric_mean(1, 4)` | `2.0` |
108
+ | `harmonic_mean(*values)` | `harmonic_mean(1, 4)` | `1.6` |
109
+
110
+ ### Errors
111
+
112
+ Everything raises `CalcError` (a plain `Exception` subclass):
113
+
114
+ - `DivisionByZeroError` for `divide(1, 0)`, `modulus(1, 0)`, `integer_divide(1, 0)`
115
+ - `EmptySequenceError` for `mean()` with no values
116
+ - `CalcError` for bad types (strings, booleans, negatives under a root, and so on)
117
+
118
+ Booleans are rejected on purpose, so `add(True, 1)` fails instead of silently
119
+ treating `True` as `1`.
120
+
121
+ ## Command line
122
+
123
+ ```powershell
124
+ calckit-kabanda add 1 2 3
125
+ calckit-kabanda divide 10 4
126
+ calckit-kabanda mean 2 4 6
127
+ calckit-kabanda median 4 1 3 2
128
+ calckit-kabanda sqrt 9
129
+ calckit-kabanda factorial 5
130
+ calckit-kabanda --help
131
+ ```
132
+
133
+ Without installing the script, use the module form:
134
+
135
+ ```powershell
136
+ python -m calc_toolkit add 1 2 3
137
+ ```
138
+
139
+ Exit codes: `0` success, `1` calculation error, `2` bad arguments.
140
+
141
+ ## Tests
142
+
143
+ ```powershell
144
+ pytest
145
+ ```
@@ -0,0 +1,116 @@
1
+ # calckit-kabanda
2
+
3
+ Arithmetic and statistics helpers for Python, plus a `calckit-kabanda` command line tool.
4
+
5
+ ## Install
6
+
7
+ From GitHub (no PyPI account needed, works immediately):
8
+
9
+ ```powershell
10
+ pip install git+https://github.com/kabanda-jordan/calckit-kabanda.git
11
+ ```
12
+
13
+ From PyPI, once published:
14
+
15
+ ```powershell
16
+ pip install calckit-kabanda
17
+ ```
18
+
19
+ Or from a checkout:
20
+
21
+ ```powershell
22
+ pip install -e .
23
+ ```
24
+
25
+ The distribution is named `calckit-kabanda` on PyPI, but the import name stays
26
+ `calc_toolkit`, so code reads:
27
+
28
+ ```python
29
+ from calc_toolkit import add, subtract, multiply, divide, mean, median
30
+ ```
31
+
32
+ ## Library use
33
+
34
+ ```python
35
+ add(1, 2, 3) # 6
36
+ subtract(10, 3, 2) # 5
37
+ multiply(2, 3, 4) # 24
38
+ divide(10, 4) # 2.5
39
+
40
+ mean(2, 4, 6) # 4.0 (same as average)
41
+ median(1, 3, 2) # 2.0
42
+ mode(1, 2, 2, 3) # 2.0
43
+ ```
44
+
45
+ ### Arithmetic
46
+
47
+ | Function | Example | Result |
48
+ | --- | --- | --- |
49
+ | `add(*values)` | `add(1, 2, 3)` | `6` |
50
+ | `subtract(a, *rest)` | `subtract(10, 3)` | `7` |
51
+ | `multiply(*values)` | `multiply(2, 3, 4)` | `24` |
52
+ | `divide(a, b)` | `divide(10, 4)` | `2.5` |
53
+ | `integer_divide(a, b)` | `integer_divide(10, 3)` | `3` |
54
+ | `power(base, exp)` | `power(2, 10)` | `1024` |
55
+ | `modulus(a, b)` | `modulus(10, 3)` | `1` |
56
+ | `absolute(x)` | `absolute(-5)` | `5` |
57
+ | `root(x, degree=2)` | `root(27, 3)` | `3.0` |
58
+ | `square_root(x)` | `square_root(9)` | `3.0` |
59
+ | `logarithm(x, base=e)` | `logarithm(8, 2)` | `3.0` |
60
+ | `factorial(n)` | `factorial(5)` | `120` |
61
+
62
+ ### Statistics
63
+
64
+ All statistics accept a variable number of values, so both
65
+ `mean([2, 4, 6])` and `mean(2, 4, 6)` work.
66
+
67
+ | Function | Example | Result |
68
+ | --- | --- | --- |
69
+ | `total(*values)` | `total(1, 2, 3)` | `6.0` |
70
+ | `mean(*values)` / `average` | `mean(2, 4, 6)` | `4.0` |
71
+ | `median(*values)` | `median(4, 1, 3, 2)` | `2.5` |
72
+ | `mode(*values)` | `mode(1, 2, 2)` | `2.0` |
73
+ | `minimum(*values)` | `min(3, 1, 2)` | `1.0` |
74
+ | `maximum(*values)` | `max(3, 1, 2)` | `3.0` |
75
+ | `value_range(*values)` | `range(3, 1, 2)` | `2.0` |
76
+ | `variance(*values, sample=False)` | `variance(2, 4, 6)` | `2.666…` |
77
+ | `standard_deviation(*values)` | `stdev(2, 4, 6)` | `1.632…` |
78
+ | `geometric_mean(*values)` | `geometric_mean(1, 4)` | `2.0` |
79
+ | `harmonic_mean(*values)` | `harmonic_mean(1, 4)` | `1.6` |
80
+
81
+ ### Errors
82
+
83
+ Everything raises `CalcError` (a plain `Exception` subclass):
84
+
85
+ - `DivisionByZeroError` for `divide(1, 0)`, `modulus(1, 0)`, `integer_divide(1, 0)`
86
+ - `EmptySequenceError` for `mean()` with no values
87
+ - `CalcError` for bad types (strings, booleans, negatives under a root, and so on)
88
+
89
+ Booleans are rejected on purpose, so `add(True, 1)` fails instead of silently
90
+ treating `True` as `1`.
91
+
92
+ ## Command line
93
+
94
+ ```powershell
95
+ calckit-kabanda add 1 2 3
96
+ calckit-kabanda divide 10 4
97
+ calckit-kabanda mean 2 4 6
98
+ calckit-kabanda median 4 1 3 2
99
+ calckit-kabanda sqrt 9
100
+ calckit-kabanda factorial 5
101
+ calckit-kabanda --help
102
+ ```
103
+
104
+ Without installing the script, use the module form:
105
+
106
+ ```powershell
107
+ python -m calc_toolkit add 1 2 3
108
+ ```
109
+
110
+ Exit codes: `0` success, `1` calculation error, `2` bad arguments.
111
+
112
+ ## Tests
113
+
114
+ ```powershell
115
+ pytest
116
+ ```
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "calckit-kabanda"
7
+ version = "0.1.0"
8
+ description = "Arithmetic and statistics helpers: add, subtract, multiply, divide, mean, median, mode and more."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "kabanda-jordan" }]
14
+ keywords = ["math", "arithmetic", "statistics", "calculator", "average", "mean"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Education",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Scientific/Engineering :: Mathematics",
29
+ "Typing :: Typed",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/kabanda-jordan/calckit-kabanda"
34
+ Repository = "https://github.com/kabanda-jordan/calckit-kabanda"
35
+ Issues = "https://github.com/kabanda-jordan/calckit-kabanda/issues"
36
+ Changelog = "https://github.com/kabanda-jordan/calckit-kabanda/blob/main/CHANGELOG.md"
37
+
38
+ [project.scripts]
39
+ calckit-kabanda = "calc_toolkit.cli:main"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/calc_toolkit"]
43
+
44
+ [dependency-groups]
45
+ dev = ["pytest>=7.0"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
@@ -0,0 +1,77 @@
1
+ """calc_toolkit - arithmetic and statistics helpers.
2
+
3
+ Examples:
4
+ >>> from calc_toolkit import add, divide, mean
5
+ >>> add(1, 2, 3)
6
+ 6
7
+ >>> divide(10, 4)
8
+ 2.5
9
+ >>> mean(2, 4, 6)
10
+ 4.0
11
+ """
12
+
13
+ from .arithmetic import (
14
+ absolute,
15
+ add,
16
+ divide,
17
+ factorial,
18
+ integer_divide,
19
+ logarithm,
20
+ modulus,
21
+ multiply,
22
+ power,
23
+ root,
24
+ square_root,
25
+ subtract,
26
+ )
27
+ from .errors import CalcError, DivisionByZeroError, EmptySequenceError
28
+ from .statistics import (
29
+ average,
30
+ geometric_mean,
31
+ harmonic_mean,
32
+ maximum,
33
+ mean,
34
+ median,
35
+ minimum,
36
+ mode,
37
+ standard_deviation,
38
+ total,
39
+ value_range,
40
+ variance,
41
+ )
42
+
43
+ __version__ = "0.1.0"
44
+
45
+ __all__ = [
46
+ # arithmetic
47
+ "add",
48
+ "subtract",
49
+ "multiply",
50
+ "divide",
51
+ "integer_divide",
52
+ "power",
53
+ "modulus",
54
+ "absolute",
55
+ "root",
56
+ "square_root",
57
+ "logarithm",
58
+ "factorial",
59
+ # statistics
60
+ "total",
61
+ "mean",
62
+ "average",
63
+ "median",
64
+ "mode",
65
+ "minimum",
66
+ "maximum",
67
+ "value_range",
68
+ "variance",
69
+ "standard_deviation",
70
+ "geometric_mean",
71
+ "harmonic_mean",
72
+ # errors
73
+ "CalcError",
74
+ "DivisionByZeroError",
75
+ "EmptySequenceError",
76
+ "__version__",
77
+ ]
@@ -0,0 +1,5 @@
1
+ """Allow ``python -m calc_toolkit``."""
2
+
3
+ from .cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,129 @@
1
+ """Basic arithmetic operations.
2
+
3
+ Every function accepts any number of values and returns a numeric result.
4
+ Numbers can be ints or floats; ``True``/``False`` are rejected on purpose so
5
+ that booleans never sneak in as 0/1.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from typing import Iterable, Union
12
+
13
+ from .errors import CalcError, DivisionByZeroError
14
+
15
+ Number = Union[int, float]
16
+
17
+
18
+ def _validate(values: Iterable[Number], op: str) -> list:
19
+ """Return ``values`` as a list of numbers, or raise ``CalcError``."""
20
+ out = []
21
+ for value in values:
22
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
23
+ raise CalcError(
24
+ f"{op}() only accepts int or float, got {type(value).__name__}: {value!r}"
25
+ )
26
+ out.append(value)
27
+ if not out:
28
+ raise CalcError(f"{op}() needs at least one value")
29
+ return out
30
+
31
+
32
+ def _check_nonzero(divisor: Number, op: str) -> None:
33
+ if divisor == 0:
34
+ raise DivisionByZeroError(f"{op}() cannot divide by zero")
35
+
36
+
37
+ def add(*values: Number) -> Number:
38
+ """Return the sum of all ``values``."""
39
+ return sum(_validate(values, "add"))
40
+
41
+
42
+ def subtract(minuend: Number, *subtrahends: Number) -> Number:
43
+ """Subtract every following value from ``minuend``.
44
+
45
+ ``subtract(10, 3, 2)`` -> ``5``
46
+ """
47
+ _validate((minuend, *subtrahends), "subtract")
48
+ result = minuend
49
+ for value in subtrahends:
50
+ result -= value
51
+ return result
52
+
53
+
54
+ def multiply(*values: Number) -> Number:
55
+ """Return the product of all ``values``."""
56
+ result = 1
57
+ for value in _validate(values, "multiply"):
58
+ result *= value
59
+ return result
60
+
61
+
62
+ def divide(dividend: Number, divisor: Number) -> Number:
63
+ """Return ``dividend / divisor``.
64
+
65
+ Raises:
66
+ DivisionByZeroError: if ``divisor`` is zero.
67
+ """
68
+ _validate((dividend, divisor), "divide")
69
+ _check_nonzero(divisor, "divide")
70
+ return dividend / divisor
71
+
72
+
73
+ def integer_divide(dividend: Number, divisor: Number) -> int:
74
+ """Return floor division (``//``) as an int."""
75
+ _validate((dividend, divisor), "integer_divide")
76
+ _check_nonzero(divisor, "integer_divide")
77
+ return int(dividend // divisor)
78
+
79
+
80
+ def power(base: Number, exponent: Number) -> Number:
81
+ """Return ``base ** exponent``."""
82
+ _validate((base, exponent), "power")
83
+ return base**exponent
84
+
85
+
86
+ def modulus(dividend: Number, divisor: Number) -> Number:
87
+ """Return the remainder (``%``)."""
88
+ _validate((dividend, divisor), "modulus")
89
+ _check_nonzero(divisor, "modulus")
90
+ return dividend % divisor
91
+
92
+
93
+ def absolute(value: Number) -> Number:
94
+ """Return the absolute value of ``value``."""
95
+ _validate((value,), "absolute")
96
+ return abs(value)
97
+
98
+
99
+ def root(value: Number, degree: Number = 2) -> float:
100
+ """Return the ``degree``-th root of a non-negative ``value``."""
101
+ _validate((value, degree), "root")
102
+ if value < 0:
103
+ raise CalcError(f"root() needs a non-negative value, got {value}")
104
+ if degree == 0:
105
+ raise CalcError("root() degree cannot be zero")
106
+ return float(value ** (1 / degree))
107
+
108
+
109
+ def square_root(value: Number) -> float:
110
+ """Return the square root of ``value``."""
111
+ return root(value, 2)
112
+
113
+
114
+ def logarithm(value: Number, base: Number = math.e) -> float:
115
+ """Return the logarithm of ``value`` in the given ``base``."""
116
+ _validate((value, base), "logarithm")
117
+ if value <= 0:
118
+ raise CalcError(f"logarithm() needs a value above zero, got {value}")
119
+ _check_nonzero(base, "logarithm")
120
+ return math.log(value, base)
121
+
122
+
123
+ def factorial(value: int) -> int:
124
+ """Return ``value!`` for non-negative integers."""
125
+ if isinstance(value, bool) or not isinstance(value, int):
126
+ raise CalcError(f"factorial() needs an int, got {type(value).__name__}")
127
+ if value < 0:
128
+ raise CalcError(f"factorial() needs a non-negative value, got {value}")
129
+ return math.factorial(value)
@@ -0,0 +1,133 @@
1
+ """Command line interface: ``calc <operation> <numbers...>``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from . import __version__
9
+ from .arithmetic import (
10
+ absolute,
11
+ add,
12
+ divide,
13
+ factorial,
14
+ integer_divide,
15
+ logarithm,
16
+ modulus,
17
+ multiply,
18
+ power,
19
+ root,
20
+ square_root,
21
+ subtract,
22
+ )
23
+ from .errors import CalcError
24
+ from .statistics import (
25
+ average,
26
+ geometric_mean,
27
+ harmonic_mean,
28
+ maximum,
29
+ mean,
30
+ median,
31
+ minimum,
32
+ mode,
33
+ standard_deviation,
34
+ total,
35
+ value_range,
36
+ variance,
37
+ )
38
+
39
+ OPERATIONS = {
40
+ "add": add,
41
+ "subtract": subtract,
42
+ "multiply": multiply,
43
+ "divide": divide,
44
+ "div": divide,
45
+ "idiv": integer_divide,
46
+ "power": power,
47
+ "pow": power,
48
+ "modulus": modulus,
49
+ "mod": modulus,
50
+ "abs": absolute,
51
+ "root": root,
52
+ "sqrt": square_root,
53
+ "log": logarithm,
54
+ "factorial": factorial,
55
+ "total": total,
56
+ "mean": mean,
57
+ "average": average,
58
+ "median": median,
59
+ "mode": mode,
60
+ "min": minimum,
61
+ "max": maximum,
62
+ "range": value_range,
63
+ "variance": variance,
64
+ "stdev": standard_deviation,
65
+ "geomean": geometric_mean,
66
+ "harmmean": harmonic_mean,
67
+ }
68
+
69
+ NEEDS_ONE = {"abs", "sqrt", "factorial"}
70
+ NEEDS_TWO = {"divide", "div", "idiv", "power", "pow", "modulus", "mod"}
71
+ ONE_OR_TWO = {"root", "log"}
72
+
73
+
74
+ def build_parser() -> argparse.ArgumentParser:
75
+ parser = argparse.ArgumentParser(
76
+ prog="calckit-kabanda",
77
+ description="Arithmetic and statistics calculator.",
78
+ epilog="Operations: " + ", ".join(sorted(OPERATIONS)),
79
+ )
80
+ parser.add_argument("operation", help="e.g. add, divide, mean")
81
+ parser.add_argument("numbers", nargs="*", help="numbers to work on")
82
+ parser.add_argument("-V", "--version", action="version", version=f"calckit-kabanda {__version__}")
83
+ return parser
84
+
85
+
86
+ def main(argv: list[str] | None = None) -> int:
87
+ args = build_parser().parse_args(argv)
88
+
89
+ op_name = args.operation.lower()
90
+ if op_name not in OPERATIONS:
91
+ print(
92
+ f"Unknown operation: {args.operation}\nOperations: " + ", ".join(sorted(OPERATIONS)),
93
+ file=sys.stderr,
94
+ )
95
+ return 2
96
+
97
+ try:
98
+ numbers = [float(n) for n in args.numbers]
99
+ except ValueError:
100
+ print("All arguments must be numbers", file=sys.stderr)
101
+ return 2
102
+
103
+ if op_name in NEEDS_ONE and len(numbers) != 1:
104
+ print(f"{op_name}() takes exactly 1 number", file=sys.stderr)
105
+ return 2
106
+ if op_name in NEEDS_TWO and len(numbers) != 2:
107
+ print(f"{op_name}() takes exactly 2 numbers", file=sys.stderr)
108
+ return 2
109
+ if op_name in ONE_OR_TWO and len(numbers) > 2:
110
+ print(f"{op_name}() takes 1 or 2 numbers", file=sys.stderr)
111
+ return 2
112
+ if not numbers and op_name not in NEEDS_ONE:
113
+ print(f"{op_name}() needs at least one number", file=sys.stderr)
114
+ return 2
115
+
116
+ if op_name == "factorial":
117
+ numbers = [numbers[0].is_integer() and int(numbers[0])]
118
+
119
+ try:
120
+ result = OPERATIONS[op_name](*numbers)
121
+ except CalcError as exc:
122
+ print(f"Error: {exc}", file=sys.stderr)
123
+ return 1
124
+
125
+ if isinstance(result, float) and result.is_integer():
126
+ print(int(result))
127
+ else:
128
+ print(result)
129
+ return 0
130
+
131
+
132
+ if __name__ == "__main__":
133
+ raise SystemExit(main())
@@ -0,0 +1,13 @@
1
+ """Custom exceptions for calc_toolkit."""
2
+
3
+
4
+ class CalcError(Exception):
5
+ """Base class for every error raised by calc_toolkit."""
6
+
7
+
8
+ class DivisionByZeroError(CalcError):
9
+ """Raised when a division would divide by zero."""
10
+
11
+
12
+ class EmptySequenceError(CalcError):
13
+ """Raised when a statistic is requested on an empty sequence."""
@@ -0,0 +1,112 @@
1
+ """Statistics over a sequence of numbers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections import Counter
7
+ from typing import Iterable, Union
8
+
9
+ from .errors import CalcError, EmptySequenceError
10
+
11
+ Number = Union[int, float]
12
+
13
+
14
+ def _clean(values: Iterable[Number], op: str) -> list[float]:
15
+ """Return ``values`` as a non-empty list of floats."""
16
+ out = []
17
+ for value in values:
18
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
19
+ raise CalcError(
20
+ f"{op}() only accepts int or float, got {type(value).__name__}: {value!r}"
21
+ )
22
+ out.append(float(value))
23
+ if not out:
24
+ raise EmptySequenceError(f"{op}() needs at least one value")
25
+ return out
26
+
27
+
28
+ def total(*values: Number) -> float:
29
+ """Return the sum of all ``values``."""
30
+ return float(sum(_clean(values, "total")))
31
+
32
+
33
+ def mean(*values: Number) -> float:
34
+ """Return the arithmetic mean (average) of ``values``.
35
+
36
+ Also available as ``average``.
37
+ """
38
+ nums = _clean(values, "mean")
39
+ return sum(nums) / len(nums)
40
+
41
+
42
+ average = mean
43
+
44
+
45
+ def median(*values: Number) -> float:
46
+ """Return the middle value (or mean of the two middles)."""
47
+ nums = sorted(_clean(values, "median"))
48
+ mid = len(nums) // 2
49
+ if len(nums) % 2:
50
+ return nums[mid]
51
+ return (nums[mid - 1] + nums[mid]) / 2
52
+
53
+
54
+ def mode(*values: Number) -> float:
55
+ """Return the most common value.
56
+
57
+ With several tied values the smallest one is returned.
58
+ """
59
+ counts = Counter(_clean(values, "mode"))
60
+ top = max(counts.values())
61
+ return min(value for value, count in counts.items() if count == top)
62
+
63
+
64
+ def minimum(*values: Number) -> float:
65
+ """Return the smallest value."""
66
+ return min(_clean(values, "minimum"))
67
+
68
+
69
+ def maximum(*values: Number) -> float:
70
+ """Return the largest value."""
71
+ return max(_clean(values, "maximum"))
72
+
73
+
74
+ def value_range(*values: Number) -> float:
75
+ """Return ``max - min``."""
76
+ nums = _clean(values, "value_range")
77
+ return max(nums) - min(nums)
78
+
79
+
80
+ def variance(*values: Number, sample: bool = False) -> float:
81
+ """Return the variance.
82
+
83
+ Args:
84
+ sample: use n-1 (Bessel's correction) instead of n.
85
+ """
86
+ nums = _clean(values, "variance")
87
+ if sample and len(nums) < 2:
88
+ raise CalcError("variance() with sample=True needs at least two values")
89
+ avg = sum(nums) / len(nums)
90
+ divisor = len(nums) - 1 if sample else len(nums)
91
+ return sum((n - avg) ** 2 for n in nums) / divisor
92
+
93
+
94
+ def standard_deviation(*values: Number, sample: bool = False) -> float:
95
+ """Return the standard deviation (square root of the variance)."""
96
+ return math.sqrt(variance(*values, sample=sample))
97
+
98
+
99
+ def geometric_mean(*values: Number) -> float:
100
+ """Return the geometric mean. All values must be positive."""
101
+ nums = _clean(values, "geometric_mean")
102
+ if any(n <= 0 for n in nums):
103
+ raise CalcError("geometric_mean() only accepts values above zero")
104
+ return math.exp(sum(math.log(n) for n in nums) / len(nums))
105
+
106
+
107
+ def harmonic_mean(*values: Number) -> float:
108
+ """Return the harmonic mean. All values must be non-zero."""
109
+ nums = _clean(values, "harmonic_mean")
110
+ if any(n == 0 for n in nums):
111
+ raise CalcError("harmonic_mean() cannot accept zero")
112
+ return len(nums) / sum(1 / n for n in nums)
@@ -0,0 +1,82 @@
1
+ import pytest
2
+
3
+ from calc_toolkit import (
4
+ CalcError,
5
+ DivisionByZeroError,
6
+ absolute,
7
+ add,
8
+ divide,
9
+ factorial,
10
+ integer_divide,
11
+ modulus,
12
+ multiply,
13
+ power,
14
+ square_root,
15
+ subtract,
16
+ )
17
+
18
+
19
+ def test_add():
20
+ assert add(1, 2, 3) == 6
21
+ assert add(1.5, 2) == 3.5
22
+
23
+
24
+ def test_add_rejects_nothing_given():
25
+ with pytest.raises(CalcError):
26
+ add()
27
+
28
+
29
+ def test_subtract():
30
+ assert subtract(10, 3) == 7
31
+ assert subtract(10, 3, 2) == 5
32
+
33
+
34
+ def test_multiply():
35
+ assert multiply(2, 3, 4) == 24
36
+ assert multiply(5) == 5
37
+
38
+
39
+ def test_divide():
40
+ assert divide(10, 4) == 2.5
41
+ assert divide(9, 3) == 3.0
42
+
43
+
44
+ def test_divide_by_zero():
45
+ with pytest.raises(DivisionByZeroError):
46
+ divide(1, 0)
47
+
48
+
49
+ def test_integer_divide():
50
+ assert integer_divide(10, 3) == 3
51
+ assert integer_divide(-10, 3) == -4
52
+
53
+
54
+ def test_power_and_modulus():
55
+ assert power(2, 10) == 1024
56
+ assert modulus(10, 3) == 1
57
+ with pytest.raises(DivisionByZeroError):
58
+ modulus(10, 0)
59
+
60
+
61
+ def test_root_and_log():
62
+ assert square_root(9) == 3.0
63
+ with pytest.raises(CalcError):
64
+ square_root(-1)
65
+
66
+
67
+ def test_absolute_and_factorial():
68
+ assert absolute(-5) == 5
69
+ assert factorial(5) == 120
70
+ assert factorial(0) == 1
71
+ with pytest.raises(CalcError):
72
+ factorial(-1)
73
+
74
+
75
+ def test_booleans_rejected():
76
+ with pytest.raises(CalcError):
77
+ add(True, 1)
78
+
79
+
80
+ def test_strings_rejected():
81
+ with pytest.raises(CalcError):
82
+ multiply("2", 3)
@@ -0,0 +1,44 @@
1
+ import pytest
2
+
3
+ from calc_toolkit.cli import main
4
+
5
+
6
+ def test_add(capsys):
7
+ assert main(["add", "1", "2", "3"]) == 0
8
+ assert capsys.readouterr().out.strip() == "6"
9
+
10
+
11
+ def test_divide_renders_without_decimal_point(capsys):
12
+ assert main(["divide", "9", "3"]) == 0
13
+ assert capsys.readouterr().out.strip() == "3"
14
+
15
+
16
+ def test_mean(capsys):
17
+ assert main(["mean", "2", "4", "6"]) == 0
18
+ assert capsys.readouterr().out.strip() == "4"
19
+
20
+
21
+ def test_divide_by_zero_returns_error(capsys):
22
+ assert main(["divide", "1", "0"]) == 1
23
+ assert "divide by zero" in capsys.readouterr().err
24
+
25
+
26
+ def test_unknown_operation(capsys):
27
+ assert main(["frobnicate", "1"]) == 2
28
+
29
+
30
+ def test_non_numeric_argument(capsys):
31
+ assert main(["add", "abc"]) == 2
32
+
33
+
34
+ def test_wrong_argument_count(capsys):
35
+ assert main(["divide", "1", "2", "3"]) == 2
36
+
37
+
38
+ def test_missing_numbers(capsys):
39
+ assert main(["add"]) == 2
40
+
41
+
42
+ def test_factorial(capsys):
43
+ assert main(["factorial", "5"]) == 0
44
+ assert capsys.readouterr().out.strip() == "120"
@@ -0,0 +1,77 @@
1
+ import math
2
+
3
+ import pytest
4
+
5
+ from calc_toolkit import (
6
+ CalcError,
7
+ EmptySequenceError,
8
+ average,
9
+ geometric_mean,
10
+ harmonic_mean,
11
+ maximum,
12
+ mean,
13
+ median,
14
+ minimum,
15
+ mode,
16
+ standard_deviation,
17
+ total,
18
+ value_range,
19
+ variance,
20
+ )
21
+
22
+
23
+ def test_total():
24
+ assert total(1, 2, 3, 4) == 10.0
25
+
26
+
27
+ def test_mean_and_average():
28
+ assert mean(2, 4, 6) == 4.0
29
+ assert average(2, 4, 6) == 4.0
30
+ assert mean(5) == 5.0
31
+
32
+
33
+ def test_median():
34
+ assert median(1, 3, 2) == 2.0
35
+ assert median(4, 1, 3, 2) == 2.5
36
+
37
+
38
+ def test_mode_picks_smallest_on_tie():
39
+ assert mode(1, 2, 2, 3) == 2.0
40
+ assert mode(1, 1, 2, 2) == 1.0
41
+
42
+
43
+ def test_min_max_range():
44
+ assert minimum(3, 1, 2) == 1.0
45
+ assert maximum(3, 1, 2) == 3.0
46
+ assert value_range(3, 1, 2) == 2.0
47
+
48
+
49
+ def test_variance_and_stdev():
50
+ assert variance(2, 4, 4, 4, 5, 5, 7, 9) == pytest.approx(4.0)
51
+ assert variance(2, 4, 4, 4, 5, 5, 7, 9, sample=True) == pytest.approx(32 / 7)
52
+ assert standard_deviation(2, 4, 4, 4, 5, 5, 7, 9) == pytest.approx(2.0)
53
+
54
+
55
+ def test_population_variance_single_value():
56
+ assert variance(7) == 0.0
57
+ with pytest.raises(CalcError):
58
+ variance(7, sample=True)
59
+
60
+
61
+ def test_means():
62
+ assert geometric_mean(1, 4) == pytest.approx(2.0)
63
+ assert harmonic_mean(1, 4) == pytest.approx(1.6)
64
+ with pytest.raises(CalcError):
65
+ geometric_mean(0)
66
+ with pytest.raises(CalcError):
67
+ harmonic_mean(0)
68
+
69
+
70
+ def test_empty_sequence():
71
+ with pytest.raises(EmptySequenceError):
72
+ mean()
73
+
74
+
75
+ def test_non_numeric():
76
+ with pytest.raises(CalcError):
77
+ median(1, "two")