quicktools-atom 0.1.1__tar.gz → 0.3.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.
- {quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/PKG-INFO +6 -2
- quicktools_atom-0.3.0/pyproject.toml +25 -0
- quicktools_atom-0.3.0/src/quicktools/__init__.py +88 -0
- quicktools_atom-0.3.0/src/quicktools/calculus.py +115 -0
- quicktools_atom-0.3.0/src/quicktools/combinatorics.py +88 -0
- quicktools_atom-0.3.0/src/quicktools/linalg.py +188 -0
- quicktools_atom-0.3.0/src/quicktools/mathtools.py +128 -0
- quicktools_atom-0.3.0/src/quicktools/numbertheory.py +149 -0
- quicktools_atom-0.3.0/src/quicktools/strtools.py +236 -0
- {quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/PKG-INFO +6 -2
- {quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/SOURCES.txt +4 -0
- quicktools_atom-0.1.1/pyproject.toml +0 -17
- quicktools_atom-0.1.1/src/quicktools/__init__.py +0 -3
- quicktools_atom-0.1.1/src/quicktools/mathtools.py +0 -50
- quicktools_atom-0.1.1/src/quicktools/strtools.py +0 -41
- {quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/README.md +0 -0
- {quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/setup.cfg +0 -0
- {quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/src/quicktools/cli.py +0 -0
- {quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/dependency_links.txt +0 -0
- {quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/entry_points.txt +0 -0
- {quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quicktools-atom
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Math and string utilities with a bundled CLI
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Math and string utilities with a bundled CLI, by AtomDev Studios
|
|
5
|
+
Author-email: Samuel Peprah <windscribe.samuel@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Samuel-Peprah-cmd/quicktools
|
|
7
|
+
Project-URL: Repository, https://github.com/Samuel-Peprah-cmd/quicktools
|
|
8
|
+
Project-URL: Documentation, https://samuel-peprah-cmd.github.io/quicktools/
|
|
5
9
|
Requires-Python: >=3.9
|
|
6
10
|
Description-Content-Type: text/markdown
|
|
7
11
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "quicktools-atom"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Math and string utilities with a bundled CLI, by AtomDev Studios"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Samuel Peprah", email = "windscribe.samuel@gmail.com" }
|
|
13
|
+
]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/Samuel-Peprah-cmd/quicktools"
|
|
18
|
+
Repository = "https://github.com/Samuel-Peprah-cmd/quicktools"
|
|
19
|
+
Documentation = "https://samuel-peprah-cmd.github.io/quicktools/"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
quicktools = "quicktools.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["src"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
<p align="center">
|
|
3
|
+
<img src="assets/AtomDev_Studios.png" alt="AtomDev Studios" width="320" style="border-radius: 8px;">
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
# quicktools
|
|
7
|
+
|
|
8
|
+
A math and string-manipulation toolkit covering linear algebra, calculus,
|
|
9
|
+
number theory, combinatorics, and text processing — built by **Samuel Peprah**
|
|
10
|
+
under **AtomDev Studios**.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## About Samuel Peprah
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="assets/sam.png" alt="Samuel Peprah" width="160" style="border-radius: 50%; object-fit: cover;">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
Samuel Peprah is a software developer, web application developer, and
|
|
21
|
+
Occupational Therapy student at the University of Ghana. Alongside his
|
|
22
|
+
healthcare education, he is also studying Computer Science at the University
|
|
23
|
+
of the People, combining knowledge from both fields to develop practical
|
|
24
|
+
technology solutions.
|
|
25
|
+
|
|
26
|
+
His interests span full-stack web development, software engineering,
|
|
27
|
+
automation, artificial intelligence, cybersecurity, and health technology.
|
|
28
|
+
He enjoys building scalable applications that solve real-world problems and
|
|
29
|
+
continuously explores new technologies to improve his skills.
|
|
30
|
+
|
|
31
|
+
Samuel has developed educational platforms, business management systems,
|
|
32
|
+
attendance systems, healthcare applications, and AI-powered solutions. He is
|
|
33
|
+
passionate about creating software that is modern, efficient, secure, and
|
|
34
|
+
user-friendly.
|
|
35
|
+
|
|
36
|
+
## About AtomDev Studios
|
|
37
|
+
|
|
38
|
+
AtomDev Studios is the personal software development brand founded by Samuel
|
|
39
|
+
Peprah. The studio specializes in designing and developing modern websites,
|
|
40
|
+
web applications, business management systems, educational platforms, and
|
|
41
|
+
custom software solutions for individuals, startups, schools, and
|
|
42
|
+
organizations.
|
|
43
|
+
|
|
44
|
+
The mission of AtomDev Studios is to transform ideas into reliable, scalable,
|
|
45
|
+
and innovative digital products that solve real-world problems while
|
|
46
|
+
delivering exceptional user experiences.
|
|
47
|
+
|
|
48
|
+
### Areas of Expertise
|
|
49
|
+
|
|
50
|
+
- Full-Stack Web Development
|
|
51
|
+
- Custom Web Applications
|
|
52
|
+
- REST API Development
|
|
53
|
+
- Database Design & Management
|
|
54
|
+
- Educational Technology Platforms
|
|
55
|
+
- Healthcare Technology Solutions
|
|
56
|
+
- AI Integration & Automation
|
|
57
|
+
- UI/UX Design
|
|
58
|
+
- Cloud Deployment & Hosting
|
|
59
|
+
- Website Maintenance & Support
|
|
60
|
+
|
|
61
|
+
### Technologies
|
|
62
|
+
|
|
63
|
+
Python • Flask • JavaScript • HTML • CSS • Tailwind CSS • Bootstrap •
|
|
64
|
+
PostgreSQL • SQLite • Git • GitHub • Cloudflare • Docker
|
|
65
|
+
|
|
66
|
+
### Connect
|
|
67
|
+
|
|
68
|
+
- LinkedIn: [samuel-peprah-a63598347](https://www.linkedin.com/in/samuel-peprah-a63598347)
|
|
69
|
+
- X (Twitter): [@legend_consult](https://x.com/legend_consult)
|
|
70
|
+
- TikTok: [@legendinlearning](https://www.tiktok.com/@legendinlearning)
|
|
71
|
+
- Instagram: [@ksapeprah](https://www.instagram.com/ksapeprah)
|
|
72
|
+
- GitHub: [Samuel-Peprah-cmd](https://github.com/Samuel-Peprah-cmd)
|
|
73
|
+
|
|
74
|
+
> "Building innovative software solutions that bridge technology with real-world impact."
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Modules
|
|
79
|
+
|
|
80
|
+
- `mathtools` — primes, GCD/LCM, mean, median, standard deviation
|
|
81
|
+
- `strtools` — palindromes, slugify, edit distance, anagrams, ciphers
|
|
82
|
+
- `linalg` — matrix operations, determinants, inverses, linear systems
|
|
83
|
+
- `calculus` — derivatives, integrals, Taylor series, Newton's method
|
|
84
|
+
- `numbertheory` — extended GCD, modular inverse, sieve, prime factorization
|
|
85
|
+
- `combinatorics` — permutations, combinations, Catalan numbers, Fibonacci
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Calculus: numerical differentiation, integration, and series approximations."""
|
|
2
|
+
import math
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def derivative(f: Callable[[float], float], x: float, h: float = 1e-6) -> float:
|
|
7
|
+
"""Approximate f'(x) using the central difference method."""
|
|
8
|
+
return (f(x + h) - f(x - h)) / (2 * h)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def second_derivative(f: Callable[[float], float], x: float, h: float = 1e-4) -> float:
|
|
12
|
+
"""Approximate f''(x) using the central difference method."""
|
|
13
|
+
return (f(x + h) - 2 * f(x) + f(x - h)) / (h ** 2)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def integral_trapezoidal(f: Callable[[float], float], a: float, b: float, n: int = 1000) -> float:
|
|
17
|
+
"""Approximate the definite integral of f from a to b using the trapezoidal rule."""
|
|
18
|
+
if n <= 0:
|
|
19
|
+
raise ValueError("n must be positive")
|
|
20
|
+
h = (b - a) / n
|
|
21
|
+
total = (f(a) + f(b)) / 2
|
|
22
|
+
for i in range(1, n):
|
|
23
|
+
total += f(a + i * h)
|
|
24
|
+
return total * h
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def integral_simpson(f: Callable[[float], float], a: float, b: float, n: int = 1000) -> float:
|
|
28
|
+
"""Approximate the definite integral of f from a to b using Simpson's rule (n must be even)."""
|
|
29
|
+
if n % 2 != 0:
|
|
30
|
+
n += 1
|
|
31
|
+
h = (b - a) / n
|
|
32
|
+
total = f(a) + f(b)
|
|
33
|
+
for i in range(1, n):
|
|
34
|
+
coeff = 4 if i % 2 != 0 else 2
|
|
35
|
+
total += coeff * f(a + i * h)
|
|
36
|
+
return total * h / 3
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def taylor_series_exp(x: float, terms: int = 15) -> float:
|
|
40
|
+
"""Approximate e^x using its Taylor series expansion around 0."""
|
|
41
|
+
return sum((x ** n) / math.factorial(n) for n in range(terms))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def taylor_series_sin(x: float, terms: int = 15) -> float:
|
|
45
|
+
"""Approximate sin(x) using its Taylor series expansion around 0."""
|
|
46
|
+
return sum(((-1) ** n) * (x ** (2 * n + 1)) / math.factorial(2 * n + 1) for n in range(terms))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def taylor_series_cos(x: float, terms: int = 15) -> float:
|
|
50
|
+
"""Approximate cos(x) using its Taylor series expansion around 0."""
|
|
51
|
+
return sum(((-1) ** n) * (x ** (2 * n)) / math.factorial(2 * n) for n in range(terms))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def newtons_method(f: Callable[[float], float], x0: float, tol: float = 1e-10, max_iter: int = 100) -> float:
|
|
55
|
+
"""Find a root of f near x0 using Newton's method."""
|
|
56
|
+
x = x0
|
|
57
|
+
for _ in range(max_iter):
|
|
58
|
+
fx = f(x)
|
|
59
|
+
if abs(fx) < tol:
|
|
60
|
+
return x
|
|
61
|
+
dfx = derivative(f, x)
|
|
62
|
+
if dfx == 0:
|
|
63
|
+
raise ZeroDivisionError("Derivative is zero; Newton's method failed")
|
|
64
|
+
x = x - fx / dfx
|
|
65
|
+
return x
|
|
66
|
+
|
|
67
|
+
def bisection_method(f, a: float, b: float, tol: float = 1e-10, max_iter: int = 200) -> float:
|
|
68
|
+
"""Find a root of f within [a, b] using the bisection method. Requires f(a) and f(b) to have opposite signs."""
|
|
69
|
+
if f(a) * f(b) > 0:
|
|
70
|
+
raise ValueError("f(a) and f(b) must have opposite signs")
|
|
71
|
+
for _ in range(max_iter):
|
|
72
|
+
mid = (a + b) / 2
|
|
73
|
+
if abs(f(mid)) < tol or (b - a) / 2 < tol:
|
|
74
|
+
return mid
|
|
75
|
+
if f(a) * f(mid) < 0:
|
|
76
|
+
b = mid
|
|
77
|
+
else:
|
|
78
|
+
a = mid
|
|
79
|
+
return (a + b) / 2
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def secant_method(f, x0: float, x1: float, tol: float = 1e-10, max_iter: int = 200) -> float:
|
|
83
|
+
"""Find a root of f near x0 and x1 using the secant method (no derivative required)."""
|
|
84
|
+
for _ in range(max_iter):
|
|
85
|
+
fx0, fx1 = f(x0), f(x1)
|
|
86
|
+
if fx1 - fx0 == 0:
|
|
87
|
+
raise ZeroDivisionError("Secant method failed: division by zero")
|
|
88
|
+
x2 = x1 - fx1 * (x1 - x0) / (fx1 - fx0)
|
|
89
|
+
if abs(x2 - x1) < tol:
|
|
90
|
+
return x2
|
|
91
|
+
x0, x1 = x1, x2
|
|
92
|
+
return x1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def partial_derivative(f, point: list[float], index: int, h: float = 1e-6) -> float:
|
|
96
|
+
"""Approximate the partial derivative of a multivariable function f at `point`,
|
|
97
|
+
with respect to the variable at position `index`."""
|
|
98
|
+
point_plus = list(point)
|
|
99
|
+
point_minus = list(point)
|
|
100
|
+
point_plus[index] += h
|
|
101
|
+
point_minus[index] -= h
|
|
102
|
+
return (f(*point_plus) - f(*point_minus)) / (2 * h)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def arc_length(f, a: float, b: float, n: int = 1000) -> float:
|
|
106
|
+
"""Approximate the arc length of y = f(x) from x=a to x=b."""
|
|
107
|
+
def integrand(x: float) -> float:
|
|
108
|
+
return math.sqrt(1 + derivative(f, x) ** 2)
|
|
109
|
+
return integral_simpson(integrand, a, b, n)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def limit_approximation(f, x: float, h: float = 1e-6) -> float:
|
|
113
|
+
"""Approximate the limit of f(t) as t approaches x, by averaging values just before and after x
|
|
114
|
+
(useful for functions undefined exactly at x, e.g. removable discontinuities)."""
|
|
115
|
+
return (f(x - h) + f(x + h)) / 2
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Combinatorics: permutations, combinations, and counting sequences."""
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def permutations_count(n: int, r: int) -> int:
|
|
6
|
+
"""Number of ways to arrange r items out of n, order matters: nPr."""
|
|
7
|
+
if r > n or r < 0:
|
|
8
|
+
return 0
|
|
9
|
+
return math.factorial(n) // math.factorial(n - r)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def combinations_count(n: int, r: int) -> int:
|
|
13
|
+
"""Number of ways to choose r items out of n, order doesn't matter: nCr."""
|
|
14
|
+
if r > n or r < 0:
|
|
15
|
+
return 0
|
|
16
|
+
return math.factorial(n) // (math.factorial(r) * math.factorial(n - r))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def catalan_number(n: int) -> int:
|
|
20
|
+
"""The nth Catalan number — counts things like valid bracket sequences and binary tree shapes."""
|
|
21
|
+
return combinations_count(2 * n, n) // (n + 1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fibonacci(n: int) -> int:
|
|
25
|
+
"""The nth Fibonacci number (0-indexed: fibonacci(0) == 0)."""
|
|
26
|
+
if n < 0:
|
|
27
|
+
raise ValueError("n must be non-negative")
|
|
28
|
+
a, b = 0, 1
|
|
29
|
+
for _ in range(n):
|
|
30
|
+
a, b = b, a + b
|
|
31
|
+
return a
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def binomial_expansion_coeffs(n: int) -> list[int]:
|
|
35
|
+
"""Coefficients of (x + y)^n, i.e. row n of Pascal's triangle."""
|
|
36
|
+
return [combinations_count(n, k) for k in range(n + 1)]
|
|
37
|
+
|
|
38
|
+
def multinomial_coefficient(n: int, ks: list[int]) -> int:
|
|
39
|
+
"""Number of ways to divide n items into groups of sizes ks (which must sum to n)."""
|
|
40
|
+
if sum(ks) != n:
|
|
41
|
+
raise ValueError("The group sizes must sum to n")
|
|
42
|
+
result = math.factorial(n)
|
|
43
|
+
for k in ks:
|
|
44
|
+
result //= math.factorial(k)
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def stirling_second_kind(n: int, k: int) -> int:
|
|
49
|
+
"""Number of ways to partition a set of n objects into k non-empty subsets."""
|
|
50
|
+
if k == 0:
|
|
51
|
+
return 1 if n == 0 else 0
|
|
52
|
+
if k > n or n < 0:
|
|
53
|
+
return 0
|
|
54
|
+
if k == n:
|
|
55
|
+
return 1
|
|
56
|
+
total = 0
|
|
57
|
+
for j in range(k + 1):
|
|
58
|
+
sign = (-1) ** (k - j)
|
|
59
|
+
total += sign * combinations_count(k, j) * (j ** n)
|
|
60
|
+
return total // math.factorial(k)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def derangements_count(n: int) -> int:
|
|
64
|
+
"""Number of permutations of n items where no item ends up in its original position."""
|
|
65
|
+
if n == 0:
|
|
66
|
+
return 1
|
|
67
|
+
if n == 1:
|
|
68
|
+
return 0
|
|
69
|
+
a, b = 1, 0
|
|
70
|
+
for i in range(2, n + 1):
|
|
71
|
+
a, b = b, (i - 1) * (a + b)
|
|
72
|
+
return b
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def integer_partitions_count(n: int) -> int:
|
|
76
|
+
"""Number of ways to write n as a sum of positive integers, order not mattering."""
|
|
77
|
+
if n < 0:
|
|
78
|
+
return 0
|
|
79
|
+
partitions = [1] + [0] * n
|
|
80
|
+
for k in range(1, n + 1):
|
|
81
|
+
for i in range(k, n + 1):
|
|
82
|
+
partitions[i] += partitions[i - k]
|
|
83
|
+
return partitions[n]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def combinations_with_replacement_count(n: int, r: int) -> int:
|
|
87
|
+
"""Number of ways to choose r items from n types, with repetition allowed and order not mattering."""
|
|
88
|
+
return combinations_count(n + r - 1, r)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Linear algebra: matrix operations, determinants, inverses, linear systems.
|
|
2
|
+
|
|
3
|
+
Matrices are represented as lists of lists, e.g. [[1, 2], [3, 4]].
|
|
4
|
+
No external dependencies (no numpy) — pure Python.
|
|
5
|
+
"""
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def matrix_add(a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
|
|
10
|
+
"""Add two matrices of the same shape."""
|
|
11
|
+
return [[a[i][j] + b[i][j] for j in range(len(a[0]))] for i in range(len(a))]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def matrix_multiply(a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
|
|
15
|
+
"""Multiply two matrices (a is m x n, b is n x p, result is m x p)."""
|
|
16
|
+
rows_a, cols_a = len(a), len(a[0])
|
|
17
|
+
rows_b, cols_b = len(b), len(b[0])
|
|
18
|
+
if cols_a != rows_b:
|
|
19
|
+
raise ValueError(f"Cannot multiply {rows_a}x{cols_a} by {rows_b}x{cols_b}")
|
|
20
|
+
result = [[0.0] * cols_b for _ in range(rows_a)]
|
|
21
|
+
for i in range(rows_a):
|
|
22
|
+
for j in range(cols_b):
|
|
23
|
+
result[i][j] = sum(a[i][k] * b[k][j] for k in range(cols_a))
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def transpose(a: list[list[float]]) -> list[list[float]]:
|
|
28
|
+
"""Return the transpose of a matrix."""
|
|
29
|
+
return [list(row) for row in zip(*a)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def identity(n: int) -> list[list[float]]:
|
|
33
|
+
"""Return the n x n identity matrix."""
|
|
34
|
+
return [[1.0 if i == j else 0.0 for j in range(n)] for i in range(n)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def determinant(a: list[list[float]]) -> float:
|
|
38
|
+
"""Determinant of a square matrix via cofactor expansion (Laplace expansion)."""
|
|
39
|
+
n = len(a)
|
|
40
|
+
if n != len(a[0]):
|
|
41
|
+
raise ValueError("Matrix must be square")
|
|
42
|
+
if n == 1:
|
|
43
|
+
return a[0][0]
|
|
44
|
+
if n == 2:
|
|
45
|
+
return a[0][0] * a[1][1] - a[0][1] * a[1][0]
|
|
46
|
+
det = 0.0
|
|
47
|
+
for col in range(n):
|
|
48
|
+
minor = [row[:col] + row[col + 1:] for row in a[1:]]
|
|
49
|
+
sign = 1 if col % 2 == 0 else -1
|
|
50
|
+
det += sign * a[0][col] * determinant(minor)
|
|
51
|
+
return det
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def inverse(a: list[list[float]]) -> list[list[float]]:
|
|
55
|
+
"""Inverse of a square matrix using Gauss-Jordan elimination."""
|
|
56
|
+
n = len(a)
|
|
57
|
+
aug = [row[:] + [1.0 if i == j else 0.0 for j in range(n)] for i, row in enumerate(a)]
|
|
58
|
+
|
|
59
|
+
for col in range(n):
|
|
60
|
+
pivot_row = max(range(col, n), key=lambda r: abs(aug[r][col]))
|
|
61
|
+
if abs(aug[pivot_row][col]) < 1e-12:
|
|
62
|
+
raise ValueError("Matrix is singular and cannot be inverted")
|
|
63
|
+
aug[col], aug[pivot_row] = aug[pivot_row], aug[col]
|
|
64
|
+
|
|
65
|
+
pivot_val = aug[col][col]
|
|
66
|
+
aug[col] = [x / pivot_val for x in aug[col]]
|
|
67
|
+
|
|
68
|
+
for r in range(n):
|
|
69
|
+
if r != col:
|
|
70
|
+
factor = aug[r][col]
|
|
71
|
+
aug[r] = [aug[r][k] - factor * aug[col][k] for k in range(2 * n)]
|
|
72
|
+
|
|
73
|
+
return [row[n:] for row in aug]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def solve_linear_system(a: list[list[float]], b: list[float]) -> list[float]:
|
|
77
|
+
"""Solve Ax = b for x, given square matrix A and vector b."""
|
|
78
|
+
n = len(a)
|
|
79
|
+
aug = [a[i][:] + [b[i]] for i in range(n)]
|
|
80
|
+
|
|
81
|
+
for col in range(n):
|
|
82
|
+
pivot_row = max(range(col, n), key=lambda r: abs(aug[r][col]))
|
|
83
|
+
if abs(aug[pivot_row][col]) < 1e-12:
|
|
84
|
+
raise ValueError("System has no unique solution (singular matrix)")
|
|
85
|
+
aug[col], aug[pivot_row] = aug[pivot_row], aug[col]
|
|
86
|
+
|
|
87
|
+
pivot_val = aug[col][col]
|
|
88
|
+
aug[col] = [x / pivot_val for x in aug[col]]
|
|
89
|
+
|
|
90
|
+
for r in range(n):
|
|
91
|
+
if r != col:
|
|
92
|
+
factor = aug[r][col]
|
|
93
|
+
aug[r] = [aug[r][k] - factor * aug[col][k] for k in range(n + 1)]
|
|
94
|
+
|
|
95
|
+
return [row[n] for row in aug]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def trace(a: list[list[float]]) -> float:
|
|
99
|
+
"""Sum of the diagonal elements of a square matrix."""
|
|
100
|
+
return sum(a[i][i] for i in range(len(a)))
|
|
101
|
+
|
|
102
|
+
def dot_product(v1: list[float], v2: list[float]) -> float:
|
|
103
|
+
"""Dot product of two equal-length vectors."""
|
|
104
|
+
if len(v1) != len(v2):
|
|
105
|
+
raise ValueError("Vectors must be the same length")
|
|
106
|
+
return sum(a * b for a, b in zip(v1, v2))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cross_product(v1: list[float], v2: list[float]) -> list[float]:
|
|
110
|
+
"""Cross product of two 3D vectors."""
|
|
111
|
+
if len(v1) != 3 or len(v2) != 3:
|
|
112
|
+
raise ValueError("Cross product requires two 3D vectors")
|
|
113
|
+
return [
|
|
114
|
+
v1[1] * v2[2] - v1[2] * v2[1],
|
|
115
|
+
v1[2] * v2[0] - v1[0] * v2[2],
|
|
116
|
+
v1[0] * v2[1] - v1[1] * v2[0],
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def vector_magnitude(v: list[float]) -> float:
|
|
121
|
+
"""Euclidean length (magnitude) of a vector."""
|
|
122
|
+
return math.sqrt(sum(x ** 2 for x in v))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def normalize_vector(v: list[float]) -> list[float]:
|
|
126
|
+
"""Scale a vector to unit length (magnitude 1)."""
|
|
127
|
+
mag = vector_magnitude(v)
|
|
128
|
+
if mag == 0:
|
|
129
|
+
raise ValueError("Cannot normalize the zero vector")
|
|
130
|
+
return [x / mag for x in v]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def vector_add(v1: list[float], v2: list[float]) -> list[float]:
|
|
134
|
+
"""Add two equal-length vectors."""
|
|
135
|
+
if len(v1) != len(v2):
|
|
136
|
+
raise ValueError("Vectors must be the same length")
|
|
137
|
+
return [a + b for a, b in zip(v1, v2)]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def vector_subtract(v1: list[float], v2: list[float]) -> list[float]:
|
|
141
|
+
"""Subtract v2 from v1 (equal-length vectors)."""
|
|
142
|
+
if len(v1) != len(v2):
|
|
143
|
+
raise ValueError("Vectors must be the same length")
|
|
144
|
+
return [a - b for a, b in zip(v1, v2)]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def scalar_multiply(a: list[list[float]], scalar: float) -> list[list[float]]:
|
|
148
|
+
"""Multiply every element of a matrix by a scalar."""
|
|
149
|
+
return [[x * scalar for x in row] for row in a]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def matrix_power(a: list[list[float]], n: int) -> list[list[float]]:
|
|
153
|
+
"""Raise a square matrix to the n-th power (n >= 0) via repeated multiplication."""
|
|
154
|
+
if n < 0:
|
|
155
|
+
raise ValueError("n must be non-negative")
|
|
156
|
+
size = len(a)
|
|
157
|
+
result = identity(size)
|
|
158
|
+
for _ in range(n):
|
|
159
|
+
result = matrix_multiply(result, a)
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def is_symmetric(a: list[list[float]]) -> bool:
|
|
164
|
+
"""Return True if a square matrix equals its own transpose."""
|
|
165
|
+
return a == transpose(a)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def rank(a: list[list[float]]) -> int:
|
|
169
|
+
"""Rank of a matrix, computed via Gaussian elimination to row echelon form."""
|
|
170
|
+
mat = [row[:] for row in a]
|
|
171
|
+
rows, cols = len(mat), len(mat[0])
|
|
172
|
+
rank_count = 0
|
|
173
|
+
for col in range(cols):
|
|
174
|
+
pivot_row = None
|
|
175
|
+
for r in range(rank_count, rows):
|
|
176
|
+
if abs(mat[r][col]) > 1e-12:
|
|
177
|
+
pivot_row = r
|
|
178
|
+
break
|
|
179
|
+
if pivot_row is None:
|
|
180
|
+
continue
|
|
181
|
+
mat[rank_count], mat[pivot_row] = mat[pivot_row], mat[rank_count]
|
|
182
|
+
for r in range(rank_count + 1, rows):
|
|
183
|
+
factor = mat[r][col] / mat[rank_count][col]
|
|
184
|
+
mat[r] = [mat[r][k] - factor * mat[rank_count][k] for k in range(cols)]
|
|
185
|
+
rank_count += 1
|
|
186
|
+
if rank_count == rows:
|
|
187
|
+
break
|
|
188
|
+
return rank_count
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Math utilities: primes, number theory, and basic statistics."""
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def is_prime(n: int) -> bool:
|
|
6
|
+
"""Return True if n is a prime number."""
|
|
7
|
+
if n < 2:
|
|
8
|
+
return False
|
|
9
|
+
for i in range(2, int(math.sqrt(n)) + 1):
|
|
10
|
+
if n % i == 0:
|
|
11
|
+
return False
|
|
12
|
+
return True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def gcd(a: int, b: int) -> int:
|
|
16
|
+
"""Greatest common divisor of a and b."""
|
|
17
|
+
return math.gcd(a, b)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def lcm(a: int, b: int) -> int:
|
|
21
|
+
"""Least common multiple of a and b."""
|
|
22
|
+
return abs(a * b) // math.gcd(a, b)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def mean(numbers: list[float]) -> float:
|
|
26
|
+
"""Arithmetic mean of a list of numbers."""
|
|
27
|
+
if not numbers:
|
|
28
|
+
raise ValueError("mean() requires at least one number")
|
|
29
|
+
return sum(numbers) / len(numbers)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def median(numbers: list[float]) -> float:
|
|
33
|
+
"""Median of a list of numbers."""
|
|
34
|
+
if not numbers:
|
|
35
|
+
raise ValueError("median() requires at least one number")
|
|
36
|
+
nums = sorted(numbers)
|
|
37
|
+
n = len(nums)
|
|
38
|
+
mid = n // 2
|
|
39
|
+
if n % 2 == 0:
|
|
40
|
+
return (nums[mid - 1] + nums[mid]) / 2
|
|
41
|
+
return nums[mid]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def std_dev(numbers: list[float]) -> float:
|
|
45
|
+
"""Population standard deviation of a list of numbers."""
|
|
46
|
+
if not numbers:
|
|
47
|
+
raise ValueError("std_dev() requires at least one number")
|
|
48
|
+
m = mean(numbers)
|
|
49
|
+
variance = sum((x - m) ** 2 for x in numbers) / len(numbers)
|
|
50
|
+
return math.sqrt(variance)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def variance(numbers: list[float]) -> float:
|
|
54
|
+
"""Population variance of a list of numbers."""
|
|
55
|
+
if not numbers:
|
|
56
|
+
raise ValueError("variance() requires at least one number")
|
|
57
|
+
m = mean(numbers)
|
|
58
|
+
return sum((x - m) ** 2 for x in numbers) / len(numbers)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_perfect_square(n: int) -> bool:
|
|
62
|
+
"""Return True if n is a perfect square."""
|
|
63
|
+
if n < 0:
|
|
64
|
+
return False
|
|
65
|
+
root = int(math.isqrt(n))
|
|
66
|
+
return root * root == n
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def harmonic_mean(numbers: list[float]) -> float:
|
|
70
|
+
"""Harmonic mean of a list of positive numbers."""
|
|
71
|
+
if not numbers:
|
|
72
|
+
raise ValueError("harmonic_mean() requires at least one number")
|
|
73
|
+
return len(numbers) / sum(1 / x for x in numbers)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def geometric_mean(numbers: list[float]) -> float:
|
|
77
|
+
"""Geometric mean of a list of positive numbers."""
|
|
78
|
+
if not numbers:
|
|
79
|
+
raise ValueError("geometric_mean() requires at least one number")
|
|
80
|
+
product = 1.0
|
|
81
|
+
for x in numbers:
|
|
82
|
+
product *= x
|
|
83
|
+
return product ** (1 / len(numbers))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def mode(numbers: list[float]) -> list[float]:
|
|
87
|
+
"""Return the most frequently occurring value(s) in a list of numbers."""
|
|
88
|
+
if not numbers:
|
|
89
|
+
raise ValueError("mode() requires at least one number")
|
|
90
|
+
counts: dict[float, int] = {}
|
|
91
|
+
for x in numbers:
|
|
92
|
+
counts[x] = counts.get(x, 0) + 1
|
|
93
|
+
max_count = max(counts.values())
|
|
94
|
+
return [val for val, cnt in counts.items() if cnt == max_count]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def percentile(numbers: list[float], p: float) -> float:
|
|
98
|
+
"""The p-th percentile (0-100) of a list of numbers, using linear interpolation."""
|
|
99
|
+
if not numbers:
|
|
100
|
+
raise ValueError("percentile() requires at least one number")
|
|
101
|
+
if not (0 <= p <= 100):
|
|
102
|
+
raise ValueError("p must be between 0 and 100")
|
|
103
|
+
data = sorted(numbers)
|
|
104
|
+
index = (p / 100) * (len(data) - 1)
|
|
105
|
+
lower = int(index)
|
|
106
|
+
upper = min(lower + 1, len(data) - 1)
|
|
107
|
+
frac = index - lower
|
|
108
|
+
return data[lower] + frac * (data[upper] - data[lower])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def covariance(x: list[float], y: list[float]) -> float:
|
|
112
|
+
"""Population covariance between two equal-length lists of numbers."""
|
|
113
|
+
if len(x) != len(y) or not x:
|
|
114
|
+
raise ValueError("covariance() requires two non-empty lists of equal length")
|
|
115
|
+
mx, my = mean(x), mean(y)
|
|
116
|
+
return sum((xi - mx) * (yi - my) for xi, yi in zip(x, y)) / len(x)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def correlation(x: list[float], y: list[float]) -> float:
|
|
120
|
+
"""Pearson correlation coefficient between two equal-length lists of numbers."""
|
|
121
|
+
return covariance(x, y) / (std_dev(x) * std_dev(y))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def data_range(numbers: list[float]) -> float:
|
|
125
|
+
"""Difference between the maximum and minimum values in a list."""
|
|
126
|
+
if not numbers:
|
|
127
|
+
raise ValueError("data_range() requires at least one number")
|
|
128
|
+
return max(numbers) - min(numbers)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Number theory: primes, modular arithmetic, and related functions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def extended_gcd(a: int, b: int) -> tuple[int, int, int]:
|
|
5
|
+
"""Return (g, x, y) such that a*x + b*y = g = gcd(a, b)."""
|
|
6
|
+
if a == 0:
|
|
7
|
+
return b, 0, 1
|
|
8
|
+
g, x1, y1 = extended_gcd(b % a, a)
|
|
9
|
+
return g, y1 - (b // a) * x1, x1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def mod_inverse(a: int, m: int) -> int:
|
|
13
|
+
"""Return the modular multiplicative inverse of a modulo m."""
|
|
14
|
+
g, x, _ = extended_gcd(a % m, m)
|
|
15
|
+
if g != 1:
|
|
16
|
+
raise ValueError(f"No modular inverse exists for {a} mod {m}")
|
|
17
|
+
return x % m
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def sieve_of_eratosthenes(limit: int) -> list[int]:
|
|
21
|
+
"""Return all prime numbers up to and including `limit`."""
|
|
22
|
+
if limit < 2:
|
|
23
|
+
return []
|
|
24
|
+
is_prime_arr = [True] * (limit + 1)
|
|
25
|
+
is_prime_arr[0] = is_prime_arr[1] = False
|
|
26
|
+
for i in range(2, int(limit ** 0.5) + 1):
|
|
27
|
+
if is_prime_arr[i]:
|
|
28
|
+
for multiple in range(i * i, limit + 1, i):
|
|
29
|
+
is_prime_arr[multiple] = False
|
|
30
|
+
return [i for i, prime in enumerate(is_prime_arr) if prime]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def prime_factors(n: int) -> dict[int, int]:
|
|
34
|
+
"""Return the prime factorization of n as {prime: exponent}."""
|
|
35
|
+
if n < 2:
|
|
36
|
+
return {}
|
|
37
|
+
factors = {}
|
|
38
|
+
d = 2
|
|
39
|
+
while d * d <= n:
|
|
40
|
+
while n % d == 0:
|
|
41
|
+
factors[d] = factors.get(d, 0) + 1
|
|
42
|
+
n //= d
|
|
43
|
+
d += 1
|
|
44
|
+
if n > 1:
|
|
45
|
+
factors[n] = factors.get(n, 0) + 1
|
|
46
|
+
return factors
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def euler_totient(n: int) -> int:
|
|
50
|
+
"""Count integers up to n that are coprime with n (Euler's totient function)."""
|
|
51
|
+
result = n
|
|
52
|
+
factors = prime_factors(n)
|
|
53
|
+
for p in factors:
|
|
54
|
+
result -= result // p
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_coprime(a: int, b: int) -> bool:
|
|
59
|
+
"""Return True if a and b share no common factors other than 1."""
|
|
60
|
+
import math
|
|
61
|
+
return math.gcd(a, b) == 1
|
|
62
|
+
|
|
63
|
+
def chinese_remainder_theorem(remainders: list[int], moduli: list[int]) -> int:
|
|
64
|
+
"""Solve a system of congruences x = remainders[i] (mod moduli[i]) for all i,
|
|
65
|
+
returning the smallest non-negative solution (moduli must be pairwise coprime)."""
|
|
66
|
+
total_modulus = 1
|
|
67
|
+
for m in moduli:
|
|
68
|
+
total_modulus *= m
|
|
69
|
+
result = 0
|
|
70
|
+
for r, m in zip(remainders, moduli):
|
|
71
|
+
partial = total_modulus // m
|
|
72
|
+
result += r * partial * mod_inverse(partial, m)
|
|
73
|
+
return result % total_modulus
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def sum_of_divisors(n: int) -> int:
|
|
77
|
+
"""Sum of all positive divisors of n, including 1 and n itself."""
|
|
78
|
+
if n < 1:
|
|
79
|
+
raise ValueError("n must be a positive integer")
|
|
80
|
+
total = 0
|
|
81
|
+
for i in range(1, int(n ** 0.5) + 1):
|
|
82
|
+
if n % i == 0:
|
|
83
|
+
total += i
|
|
84
|
+
if i != n // i:
|
|
85
|
+
total += n // i
|
|
86
|
+
return total
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_perfect_number(n: int) -> bool:
|
|
90
|
+
"""Return True if n equals the sum of its proper divisors (e.g. 6, 28, 496)."""
|
|
91
|
+
if n < 1:
|
|
92
|
+
return False
|
|
93
|
+
return sum_of_divisors(n) - n == n
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_amicable_pair(a: int, b: int) -> bool:
|
|
97
|
+
"""Return True if a and b form an amicable pair (each equals the sum of the other's proper divisors)."""
|
|
98
|
+
return (sum_of_divisors(a) - a == b) and (sum_of_divisors(b) - b == a) and a != b
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def digital_root(n: int) -> int:
|
|
102
|
+
"""Repeatedly sum the digits of n until a single digit remains."""
|
|
103
|
+
n = abs(n)
|
|
104
|
+
while n >= 10:
|
|
105
|
+
n = sum(int(d) for d in str(n))
|
|
106
|
+
return n
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def collatz_sequence(n: int) -> list[int]:
|
|
110
|
+
"""Return the Collatz sequence starting at n, ending at 1."""
|
|
111
|
+
if n < 1:
|
|
112
|
+
raise ValueError("n must be a positive integer")
|
|
113
|
+
sequence = [n]
|
|
114
|
+
while n != 1:
|
|
115
|
+
n = n // 2 if n % 2 == 0 else 3 * n + 1
|
|
116
|
+
sequence.append(n)
|
|
117
|
+
return sequence
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def next_prime(n: int) -> int:
|
|
121
|
+
"""Return the smallest prime strictly greater than n."""
|
|
122
|
+
candidate = n + 1
|
|
123
|
+
while True:
|
|
124
|
+
is_p = True
|
|
125
|
+
for i in range(2, int(candidate ** 0.5) + 1):
|
|
126
|
+
if candidate % i == 0:
|
|
127
|
+
is_p = False
|
|
128
|
+
break
|
|
129
|
+
if is_p and candidate > 1:
|
|
130
|
+
return candidate
|
|
131
|
+
candidate += 1
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def nth_prime(n: int) -> int:
|
|
135
|
+
"""Return the nth prime number (1-indexed: nth_prime(1) == 2)."""
|
|
136
|
+
if n < 1:
|
|
137
|
+
raise ValueError("n must be a positive integer")
|
|
138
|
+
count = 0
|
|
139
|
+
candidate = 1
|
|
140
|
+
while count < n:
|
|
141
|
+
candidate = next_prime(candidate)
|
|
142
|
+
count += 1
|
|
143
|
+
return candidate
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_palindromic_number(n: int) -> bool:
|
|
147
|
+
"""Return True if n reads the same forwards and backwards."""
|
|
148
|
+
s = str(n)
|
|
149
|
+
return s == s[::-1]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""String utilities: text checks, transforms, and simple ciphers."""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def is_palindrome(text: str) -> bool:
|
|
6
|
+
"""Return True if text reads the same forwards and backwards (ignoring case/spaces)."""
|
|
7
|
+
cleaned = re.sub(r"[^a-zA-Z0-9]", "", text).lower()
|
|
8
|
+
return cleaned == cleaned[::-1]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def slugify(text: str) -> str:
|
|
12
|
+
"""Convert text into a url-friendly slug, e.g. 'Hello World!' -> 'hello-world'."""
|
|
13
|
+
text = text.lower().strip()
|
|
14
|
+
text = re.sub(r"[^a-z0-9]+", "-", text)
|
|
15
|
+
return text.strip("-")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def word_frequency(text: str) -> dict[str, int]:
|
|
19
|
+
"""Count occurrences of each word in text (case-insensitive)."""
|
|
20
|
+
words = re.findall(r"[a-zA-Z']+", text.lower())
|
|
21
|
+
freq: dict[str, int] = {}
|
|
22
|
+
for w in words:
|
|
23
|
+
freq[w] = freq.get(w, 0) + 1
|
|
24
|
+
return freq
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def caesar_cipher(text: str, shift: int) -> str:
|
|
28
|
+
"""Shift each letter in text by `shift` positions (Caesar cipher). Negative shift decodes."""
|
|
29
|
+
result = []
|
|
30
|
+
for ch in text:
|
|
31
|
+
if ch.isalpha():
|
|
32
|
+
base = ord('A') if ch.isupper() else ord('a')
|
|
33
|
+
result.append(chr((ord(ch) - base + shift) % 26 + base))
|
|
34
|
+
else:
|
|
35
|
+
result.append(ch)
|
|
36
|
+
return "".join(result)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def reverse_words(text: str) -> str:
|
|
40
|
+
"""Reverse the order of words in a sentence."""
|
|
41
|
+
return " ".join(text.split()[::-1])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def levenshtein_distance(s1: str, s2: str) -> int:
|
|
45
|
+
"""Minimum number of single-character edits (insertions, deletions, substitutions)
|
|
46
|
+
needed to turn s1 into s2."""
|
|
47
|
+
if len(s1) < len(s2):
|
|
48
|
+
return levenshtein_distance(s2, s1)
|
|
49
|
+
if len(s2) == 0:
|
|
50
|
+
return len(s1)
|
|
51
|
+
|
|
52
|
+
previous_row = list(range(len(s2) + 1))
|
|
53
|
+
for i, c1 in enumerate(s1):
|
|
54
|
+
current_row = [i + 1]
|
|
55
|
+
for j, c2 in enumerate(s2):
|
|
56
|
+
insertions = previous_row[j + 1] + 1
|
|
57
|
+
deletions = current_row[j] + 1
|
|
58
|
+
substitutions = previous_row[j] + (c1 != c2)
|
|
59
|
+
current_row.append(min(insertions, deletions, substitutions))
|
|
60
|
+
previous_row = current_row
|
|
61
|
+
return previous_row[-1]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_anagram(s1: str, s2: str) -> bool:
|
|
65
|
+
"""Return True if s1 and s2 use exactly the same letters (ignoring case/spaces)."""
|
|
66
|
+
clean1 = re.sub(r"[^a-z0-9]", "", s1.lower())
|
|
67
|
+
clean2 = re.sub(r"[^a-z0-9]", "", s2.lower())
|
|
68
|
+
return sorted(clean1) == sorted(clean2)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def to_snake_case(text: str) -> str:
|
|
72
|
+
"""Convert 'camelCase' or 'Title Case' text into 'snake_case'."""
|
|
73
|
+
text = re.sub(r"(?<!^)(?=[A-Z])", "_", text)
|
|
74
|
+
text = re.sub(r"[\s\-]+", "_", text)
|
|
75
|
+
return text.lower().strip("_")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def to_camel_case(text: str) -> str:
|
|
79
|
+
"""Convert 'snake_case' or 'kebab-case' text into 'camelCase'."""
|
|
80
|
+
parts = re.split(r"[_\-\s]+", text.strip())
|
|
81
|
+
if not parts:
|
|
82
|
+
return ""
|
|
83
|
+
return parts[0].lower() + "".join(p.capitalize() for p in parts[1:])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def rot13(text: str) -> str:
|
|
87
|
+
"""Apply the ROT13 cipher (Caesar cipher with a fixed shift of 13)."""
|
|
88
|
+
return caesar_cipher(text, 13)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def vigenere_cipher(text: str, key: str, decode: bool = False) -> str:
|
|
92
|
+
"""Encode or decode text using the Vigenere cipher with the given key."""
|
|
93
|
+
result = []
|
|
94
|
+
key = key.lower()
|
|
95
|
+
key_index = 0
|
|
96
|
+
for ch in text:
|
|
97
|
+
if ch.isalpha():
|
|
98
|
+
shift = ord(key[key_index % len(key)]) - ord('a')
|
|
99
|
+
if decode:
|
|
100
|
+
shift = -shift
|
|
101
|
+
base = ord('A') if ch.isupper() else ord('a')
|
|
102
|
+
result.append(chr((ord(ch) - base + shift) % 26 + base))
|
|
103
|
+
key_index += 1
|
|
104
|
+
else:
|
|
105
|
+
result.append(ch)
|
|
106
|
+
return "".join(result)
|
|
107
|
+
|
|
108
|
+
def longest_common_subsequence(s1: str, s2: str) -> str:
|
|
109
|
+
"""The longest sequence of characters that appears in both strings, in order (not necessarily contiguous)."""
|
|
110
|
+
m, n = len(s1), len(s2)
|
|
111
|
+
dp = [[""] * (n + 1) for _ in range(m + 1)]
|
|
112
|
+
for i in range(1, m + 1):
|
|
113
|
+
for j in range(1, n + 1):
|
|
114
|
+
if s1[i - 1] == s2[j - 1]:
|
|
115
|
+
dp[i][j] = dp[i - 1][j - 1] + s1[i - 1]
|
|
116
|
+
else:
|
|
117
|
+
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], key=len)
|
|
118
|
+
return dp[m][n]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def run_length_encode(text: str) -> str:
|
|
122
|
+
"""Compress text using run-length encoding, e.g. 'aaabbc' -> '3a2b1c'."""
|
|
123
|
+
if not text:
|
|
124
|
+
return ""
|
|
125
|
+
result = []
|
|
126
|
+
count = 1
|
|
127
|
+
prev = text[0]
|
|
128
|
+
for ch in text[1:]:
|
|
129
|
+
if ch == prev:
|
|
130
|
+
count += 1
|
|
131
|
+
else:
|
|
132
|
+
result.append(f"{count}{prev}")
|
|
133
|
+
prev = ch
|
|
134
|
+
count = 1
|
|
135
|
+
result.append(f"{count}{prev}")
|
|
136
|
+
return "".join(result)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def run_length_decode(encoded: str) -> str:
|
|
140
|
+
"""Decompress a run-length encoded string produced by run_length_encode()."""
|
|
141
|
+
result = []
|
|
142
|
+
count_str = ""
|
|
143
|
+
for ch in encoded:
|
|
144
|
+
if ch.isdigit():
|
|
145
|
+
count_str += ch
|
|
146
|
+
else:
|
|
147
|
+
result.append(ch * int(count_str))
|
|
148
|
+
count_str = ""
|
|
149
|
+
return "".join(result)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def jaccard_similarity(s1: str, s2: str) -> float:
|
|
153
|
+
"""Word-level similarity between two texts: size of the word intersection over the union (0 to 1)."""
|
|
154
|
+
words1 = set(re.findall(r"[a-zA-Z']+", s1.lower()))
|
|
155
|
+
words2 = set(re.findall(r"[a-zA-Z']+", s2.lower()))
|
|
156
|
+
if not words1 and not words2:
|
|
157
|
+
return 1.0
|
|
158
|
+
return len(words1 & words2) / len(words1 | words2)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def word_count(text: str) -> int:
|
|
162
|
+
"""Count the number of words in text."""
|
|
163
|
+
return len(re.findall(r"[a-zA-Z']+", text))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def char_frequency(text: str) -> dict[str, int]:
|
|
167
|
+
"""Count occurrences of each character in text (case-sensitive, includes all characters)."""
|
|
168
|
+
freq: dict[str, int] = {}
|
|
169
|
+
for ch in text:
|
|
170
|
+
freq[ch] = freq.get(ch, 0) + 1
|
|
171
|
+
return freq
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def most_common_word(text: str) -> str:
|
|
175
|
+
"""Return the most frequently occurring word in text (case-insensitive)."""
|
|
176
|
+
freq = word_frequency(text)
|
|
177
|
+
if not freq:
|
|
178
|
+
raise ValueError("Text contains no words")
|
|
179
|
+
return max(freq, key=freq.get)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def title_case(text: str) -> str:
|
|
183
|
+
"""Capitalize the first letter of every word in text."""
|
|
184
|
+
return " ".join(w.capitalize() for w in text.split())
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def remove_vowels(text: str) -> str:
|
|
188
|
+
"""Remove all vowels (a, e, i, o, u) from text, case-insensitive."""
|
|
189
|
+
return re.sub(r"[aeiouAEIOU]", "", text)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def count_vowels_consonants(text: str) -> tuple[int, int]:
|
|
193
|
+
"""Count the number of vowels and consonants in text (letters only)."""
|
|
194
|
+
vowels = sum(1 for ch in text.lower() if ch in "aeiou")
|
|
195
|
+
consonants = sum(1 for ch in text.lower() if ch.isalpha() and ch not in "aeiou")
|
|
196
|
+
return vowels, consonants
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def hash_text(text: str, algorithm: str = "sha256") -> str:
|
|
200
|
+
"""Return the hex digest of text using the given hash algorithm (e.g. 'md5', 'sha1', 'sha256')."""
|
|
201
|
+
import hashlib
|
|
202
|
+
h = hashlib.new(algorithm)
|
|
203
|
+
h.update(text.encode("utf-8"))
|
|
204
|
+
return h.hexdigest()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def is_pangram(text: str) -> bool:
|
|
208
|
+
"""Return True if text contains every letter of the alphabet at least once."""
|
|
209
|
+
letters = set(ch for ch in text.lower() if ch.isalpha())
|
|
210
|
+
return len(letters) == 26
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def wrap_text(text: str, width: int = 70) -> str:
|
|
214
|
+
"""Wrap text so no line exceeds `width` characters."""
|
|
215
|
+
import textwrap
|
|
216
|
+
return textwrap.fill(text, width=width)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def longest_word(text: str) -> str:
|
|
220
|
+
"""Return the longest word in text (first one found in case of a tie)."""
|
|
221
|
+
words = re.findall(r"[a-zA-Z']+", text)
|
|
222
|
+
if not words:
|
|
223
|
+
raise ValueError("Text contains no words")
|
|
224
|
+
return max(words, key=len)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def base64_encode(text: str) -> str:
|
|
228
|
+
"""Encode text into base64."""
|
|
229
|
+
import base64
|
|
230
|
+
return base64.b64encode(text.encode("utf-8")).decode("ascii")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def base64_decode(encoded: str) -> str:
|
|
234
|
+
"""Decode a base64-encoded string back into text."""
|
|
235
|
+
import base64
|
|
236
|
+
return base64.b64decode(encoded.encode("ascii")).decode("utf-8")
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quicktools-atom
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Math and string utilities with a bundled CLI
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Math and string utilities with a bundled CLI, by AtomDev Studios
|
|
5
|
+
Author-email: Samuel Peprah <windscribe.samuel@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Samuel-Peprah-cmd/quicktools
|
|
7
|
+
Project-URL: Repository, https://github.com/Samuel-Peprah-cmd/quicktools
|
|
8
|
+
Project-URL: Documentation, https://samuel-peprah-cmd.github.io/quicktools/
|
|
5
9
|
Requires-Python: >=3.9
|
|
6
10
|
Description-Content-Type: text/markdown
|
|
7
11
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
src/quicktools/__init__.py
|
|
4
|
+
src/quicktools/calculus.py
|
|
4
5
|
src/quicktools/cli.py
|
|
6
|
+
src/quicktools/combinatorics.py
|
|
7
|
+
src/quicktools/linalg.py
|
|
5
8
|
src/quicktools/mathtools.py
|
|
9
|
+
src/quicktools/numbertheory.py
|
|
6
10
|
src/quicktools/strtools.py
|
|
7
11
|
src/quicktools_atom.egg-info/PKG-INFO
|
|
8
12
|
src/quicktools_atom.egg-info/SOURCES.txt
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "quicktools-atom"
|
|
7
|
-
version = "0.1.1"
|
|
8
|
-
description = "Math and string utilities with a bundled CLI"
|
|
9
|
-
requires-python = ">=3.9"
|
|
10
|
-
readme = "README.md"
|
|
11
|
-
dependencies = []
|
|
12
|
-
|
|
13
|
-
[project.scripts]
|
|
14
|
-
quicktools = "quicktools.cli:main"
|
|
15
|
-
|
|
16
|
-
[tool.setuptools.packages.find]
|
|
17
|
-
where = ["src"]
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Math utilities: primes, number theory, and basic statistics."""
|
|
2
|
-
import math
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def is_prime(n: int) -> bool:
|
|
6
|
-
"""Return True if n is a prime number."""
|
|
7
|
-
if n < 2:
|
|
8
|
-
return False
|
|
9
|
-
for i in range(2, int(math.sqrt(n)) + 1):
|
|
10
|
-
if n % i == 0:
|
|
11
|
-
return False
|
|
12
|
-
return True
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def gcd(a: int, b: int) -> int:
|
|
16
|
-
"""Greatest common divisor of a and b."""
|
|
17
|
-
return math.gcd(a, b)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def lcm(a: int, b: int) -> int:
|
|
21
|
-
"""Least common multiple of a and b."""
|
|
22
|
-
return abs(a * b) // math.gcd(a, b)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def mean(numbers: list[float]) -> float:
|
|
26
|
-
"""Arithmetic mean of a list of numbers."""
|
|
27
|
-
if not numbers:
|
|
28
|
-
raise ValueError("mean() requires at least one number")
|
|
29
|
-
return sum(numbers) / len(numbers)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def median(numbers: list[float]) -> float:
|
|
33
|
-
"""Median of a list of numbers."""
|
|
34
|
-
if not numbers:
|
|
35
|
-
raise ValueError("median() requires at least one number")
|
|
36
|
-
nums = sorted(numbers)
|
|
37
|
-
n = len(nums)
|
|
38
|
-
mid = n // 2
|
|
39
|
-
if n % 2 == 0:
|
|
40
|
-
return (nums[mid - 1] + nums[mid]) / 2
|
|
41
|
-
return nums[mid]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def std_dev(numbers: list[float]) -> float:
|
|
45
|
-
"""Population standard deviation of a list of numbers."""
|
|
46
|
-
if not numbers:
|
|
47
|
-
raise ValueError("std_dev() requires at least one number")
|
|
48
|
-
m = mean(numbers)
|
|
49
|
-
variance = sum((x - m) ** 2 for x in numbers) / len(numbers)
|
|
50
|
-
return math.sqrt(variance)
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""String utilities: text checks, transforms, and simple ciphers."""
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def is_palindrome(text: str) -> bool:
|
|
6
|
-
"""Return True if text reads the same forwards and backwards (ignoring case/spaces)."""
|
|
7
|
-
cleaned = re.sub(r"[^a-zA-Z0-9]", "", text).lower()
|
|
8
|
-
return cleaned == cleaned[::-1]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def slugify(text: str) -> str:
|
|
12
|
-
"""Convert text into a url-friendly slug, e.g. 'Hello World!' -> 'hello-world'."""
|
|
13
|
-
text = text.lower().strip()
|
|
14
|
-
text = re.sub(r"[^a-z0-9]+", "-", text)
|
|
15
|
-
return text.strip("-")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def word_frequency(text: str) -> dict[str, int]:
|
|
19
|
-
"""Count occurrences of each word in text (case-insensitive)."""
|
|
20
|
-
words = re.findall(r"[a-zA-Z']+", text.lower())
|
|
21
|
-
freq: dict[str, int] = {}
|
|
22
|
-
for w in words:
|
|
23
|
-
freq[w] = freq.get(w, 0) + 1
|
|
24
|
-
return freq
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def caesar_cipher(text: str, shift: int) -> str:
|
|
28
|
-
"""Shift each letter in text by `shift` positions (Caesar cipher). Negative shift decodes."""
|
|
29
|
-
result = []
|
|
30
|
-
for ch in text:
|
|
31
|
-
if ch.isalpha():
|
|
32
|
-
base = ord('A') if ch.isupper() else ord('a')
|
|
33
|
-
result.append(chr((ord(ch) - base + shift) % 26 + base))
|
|
34
|
-
else:
|
|
35
|
-
result.append(ch)
|
|
36
|
-
return "".join(result)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def reverse_words(text: str) -> str:
|
|
40
|
-
"""Reverse the order of words in a sentence."""
|
|
41
|
-
return " ".join(text.split()[::-1])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{quicktools_atom-0.1.1 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|