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.
- calckit_kabanda-0.1.0/.github/workflows/publish.yml +27 -0
- calckit_kabanda-0.1.0/.github/workflows/tests.yml +30 -0
- calckit_kabanda-0.1.0/.gitignore +11 -0
- calckit_kabanda-0.1.0/CHANGELOG.md +20 -0
- calckit_kabanda-0.1.0/LICENSE +21 -0
- calckit_kabanda-0.1.0/PKG-INFO +145 -0
- calckit_kabanda-0.1.0/README.md +116 -0
- calckit_kabanda-0.1.0/pyproject.toml +48 -0
- calckit_kabanda-0.1.0/src/calc_toolkit/__init__.py +77 -0
- calckit_kabanda-0.1.0/src/calc_toolkit/__main__.py +5 -0
- calckit_kabanda-0.1.0/src/calc_toolkit/arithmetic.py +129 -0
- calckit_kabanda-0.1.0/src/calc_toolkit/cli.py +133 -0
- calckit_kabanda-0.1.0/src/calc_toolkit/errors.py +13 -0
- calckit_kabanda-0.1.0/src/calc_toolkit/statistics.py +112 -0
- calckit_kabanda-0.1.0/tests/test_arithmetic.py +82 -0
- calckit_kabanda-0.1.0/tests/test_cli.py +44 -0
- calckit_kabanda-0.1.0/tests/test_statistics.py +77 -0
|
@@ -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,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,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")
|