quicktools-atom 0.2.0__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.
Files changed (23) hide show
  1. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/PKG-INFO +6 -2
  2. quicktools_atom-0.3.0/pyproject.toml +25 -0
  3. quicktools_atom-0.3.0/src/quicktools/__init__.py +88 -0
  4. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/src/quicktools/calculus.py +51 -1
  5. quicktools_atom-0.3.0/src/quicktools/combinatorics.py +88 -0
  6. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/src/quicktools/linalg.py +90 -1
  7. quicktools_atom-0.3.0/src/quicktools/mathtools.py +128 -0
  8. quicktools_atom-0.3.0/src/quicktools/numbertheory.py +149 -0
  9. quicktools_atom-0.3.0/src/quicktools/strtools.py +236 -0
  10. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/PKG-INFO +6 -2
  11. quicktools_atom-0.2.0/pyproject.toml +0 -17
  12. quicktools_atom-0.2.0/src/quicktools/__init__.py +0 -3
  13. quicktools_atom-0.2.0/src/quicktools/combinatorics.py +0 -36
  14. quicktools_atom-0.2.0/src/quicktools/mathtools.py +0 -50
  15. quicktools_atom-0.2.0/src/quicktools/numbertheory.py +0 -61
  16. quicktools_atom-0.2.0/src/quicktools/strtools.py +0 -106
  17. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/README.md +0 -0
  18. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/setup.cfg +0 -0
  19. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/src/quicktools/cli.py +0 -0
  20. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/SOURCES.txt +0 -0
  21. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/dependency_links.txt +0 -0
  22. {quicktools_atom-0.2.0 → quicktools_atom-0.3.0}/src/quicktools_atom.egg-info/entry_points.txt +0 -0
  23. {quicktools_atom-0.2.0 → 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.2.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"
@@ -62,4 +62,54 @@ def newtons_method(f: Callable[[float], float], x0: float, tol: float = 1e-10, m
62
62
  if dfx == 0:
63
63
  raise ZeroDivisionError("Derivative is zero; Newton's method failed")
64
64
  x = x - fx / dfx
65
- return x
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)
@@ -3,6 +3,7 @@
3
3
  Matrices are represented as lists of lists, e.g. [[1, 2], [3, 4]].
4
4
  No external dependencies (no numpy) — pure Python.
5
5
  """
6
+ import math
6
7
 
7
8
 
8
9
  def matrix_add(a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
@@ -96,4 +97,92 @@ def solve_linear_system(a: list[list[float]], b: list[float]) -> list[float]:
96
97
 
97
98
  def trace(a: list[list[float]]) -> float:
98
99
  """Sum of the diagonal elements of a square matrix."""
99
- return sum(a[i][i] for i in range(len(a)))
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.2.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,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.2.0"
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,3 +0,0 @@
1
- """quicktools: a small toolkit with math and string utilities."""
2
-
3
- __version__ = "0.1.0"
@@ -1,36 +0,0 @@
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)]
@@ -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,61 +0,0 @@
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
@@ -1,106 +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])
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)