Coverage for src/pyfplib/functools.py: 100%
135 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-30 03:31 +0300
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-30 03:31 +0300
1# SPDX-FileCopyrightText: 2026-present Comet11x
2# SPDX-License-Identifier: MIT
3"""
4functools.py
6This module provides a set of functions.
8Example usage:
10 from pyfplib import
11 result = add(5, 3)
13Functions:
14- add(a, b): Returns the sum of a and b.
15- subtract(a, b): Returns the difference of a and b.
16- multiply(a, b): Returns the product of a and b.
17- divide(a, b): Returns the quotient of a and b. Raises ValueError for division by zero.
18"""
20import inspect
21from collections.abc import Callable, Iterable, Sequence, Sized
22from typing import Any, cast, overload
24from pyfplib.option import Nothing, Option, Some
27class _FnWrapper[T, R]:
28 # Number of parameters accepted by the wrapped callback.
29 ONE = 1
30 TWO = 2
31 THREE = 3
32 FORE = 4
34 def __init__(self, fn: Callable[..., R]) -> None:
35 # Inspect the callback once so calls can dispatch on its arity.
36 self._fn = fn
37 self._sig = inspect.signature(fn)
38 self._params_num = len(self._sig.parameters)
40 @property
41 def signature(self) -> inspect.Signature:
42 return self._sig
44 @property
45 def params_number(self) -> int:
46 return self._params_num
48 def __call__(self, v: T, i: int = 0, c: Sequence[T] | None = None) -> R:
49 # Forward only the arguments the wrapped callback declares.
50 if self._params_num == _FnWrapper.ONE:
51 return self._fn(v)
52 elif self._params_num == _FnWrapper.TWO:
53 return self._fn(v, i)
54 else:
55 c = c if c is not None else ()
56 return self._fn(v, i, c)
59class _FnWrapper2[T, R](_FnWrapper[T, R]):
60 def __init__(self, fn: Callable[..., R], *, pipeline: Iterable[Callable[..., R]] = []) -> None:
61 super().__init__(fn)
62 self._pipeline = pipeline
64 def __call__(self, v: T, i: int = 0, c: Sequence[T] | None = None) -> R:
65 # Run the base callback, then pass its result through each pipeline step.
66 ret_value = super().__call__(v, i, c)
67 for fn in self._pipeline:
68 ret_value = fn(ret_value)
69 return ret_value
72class _FnWrapper3[T, R]:
73 # Like _FnWrapper, but keeps the accumulated value as the first argument.
74 def __init__(self, fn: Callable[..., R]) -> None:
75 self._fn = fn
76 self._sig = inspect.signature(fn)
77 self._params_num = len(self._sig.parameters)
79 @property
80 def params_number(self) -> int:
81 return self._params_num
83 def __call__(self, p: R | T | None, v: T, i: int = 0, c: Sequence[T] | None = None) -> R:
84 # Dispatch on arity, always treating `p` as the accumulated value.
85 if self._params_num == _FnWrapper.ONE:
86 return self._fn(v)
87 elif self._params_num == _FnWrapper.TWO:
88 return self._fn(p, v)
89 elif self._params_num == _FnWrapper.THREE:
90 return self._fn(p, v, i)
91 else:
92 return self._fn(p, v, i, c)
95@overload
96def for_each[T](function: Callable[[T], None], sequence: Sequence[T]) -> None: ...
99@overload
100def for_each[T](function: Callable[[T, int], None], sequence: Sequence[T]) -> None: ...
103def for_each[T](function: Callable[[T, int, Sequence[T]], None], sequence: Sequence[T]) -> None:
104 """Applies the given function for each elements of the given sequence object
106 Args:
107 function: a callback to execute with elements
108 sequence: provides elements to apply the callback
110 Returns:
111 None
112 """
113 if is_empty(sequence):
114 return
116 wrapper = _FnWrapper(function)
118 # Pass the index (and the sequence) only when the callback accepts them.
119 if wrapper.params_number == _FnWrapper.ONE:
120 for item in sequence:
121 wrapper(item)
122 else:
123 for idx, item in enumerate(sequence):
124 wrapper(item, idx, sequence)
127@overload
128def any_of[T](predicate: Callable[[T], bool], sequence: Sequence[T]): ...
131@overload
132def any_of[T](predicate: Callable[[T, int], bool], sequence: Sequence[T]): ...
135def any_of[T](predicate: Callable[[T, int, Sequence[T]], bool], sequence: Sequence[T]) -> bool:
136 """
137 Checks if any elements of an iterable object satisfy a given condition.
139 It returns false if an iterable oject is empty.
141 Args:
142 predicate: A function checking elements.
143 iterable: An iterable object providing elements for checking.
145 Returns:
146 bool: The result of the checking.
147 """
148 # Stop as soon as the predicate returns a truthy value.
149 return _abstract_check(predicate=_FnWrapper(predicate), sequence=sequence, init_value=False, check=lambda v: v)
152@overload
153def all_of[T](predicate: Callable[[T], bool], iterable: Iterable[T]): ...
156@overload
157def all_of[T](predicate: Callable[[T, int], bool], iterable: Iterable[T]): ...
160def all_of[T](predicate: Callable[[T, int, Sequence[T]], bool], sequence: Sequence[T]) -> bool:
161 """
162 Checks if every element of an iterable object satisfy a given condition.
164 It returns true if an iterable object is empty.
166 Args:
167 predicate: a function checking elements.
168 iterable: an iterable object providing elements for checking.
170 Returns:
171 bool: The result of the checking.
172 """
173 # Stop as soon as the predicate returns a falsy value.
174 return _abstract_check(predicate=_FnWrapper(predicate), sequence=sequence, init_value=True, check=lambda v: not v)
177@overload
178def none_of[T](predicate: Callable[[T], bool], iterable: Iterable[T]): ...
181@overload
182def none_of[T](predicate: Callable[[T, int], bool], iterable: Iterable[T]): ...
185def none_of[T](callback: Callable[[T], bool], sequence: Sequence[T]) -> bool:
186 """
187 Checks if no one element of an iterable object not satisfy a given condition.
189 It returns true if an iterable object is empty.
191 Args:
192 predicate: a function checking elements.
193 sequence: a sequence object providing elements for checking.
195 Returns:
196 bool: The result of the checking.
197 """
198 # Invert the callback's result via the pipeline, then stop on the first match.
199 return _abstract_check(
200 predicate=_FnWrapper2(callback, pipeline=(lambda v: not v,)),
201 sequence=sequence,
202 init_value=True,
203 check=lambda v: not v,
204 )
207# @overload
208# def fold[T](
209# function: Callable[[T, T], T],
210# sequence: Sequence[T],
211# initial: T | None = None,
212# ) -> Option[T]: ...
215def fold[T, R](
216 function: Callable[[Option[R], T], R] | Callable[[Option[R], T, int], R],
217 sequence: Sequence[T],
218 initial: R | None = None,
219) -> Option[R]:
220 """
221 Folds the sequence into a single accumulated value.
223 Args:
224 function: Callback receiving the accumulator and the current item, and optionally its index.
225 sequence: Elements to fold over.
226 initial: Optional starting value for the accumulator.
228 Returns:
229 Option[R]: The final accumulator value, or Nothing if the sequence is empty and no initial value is given.
230 """
231 # Seed the accumulator with the optional initial value.
232 ret_value = Option.from_optional(initial)
233 if len(sequence) != 0:
234 wrapper = _FnWrapper3(function)
235 # Two-parameter callbacks receive the accumulator and the current item.
236 if wrapper.params_number == _FnWrapper.TWO:
237 fn = cast(Callable[[Option[R], T], R], function)
238 for item in sequence:
239 ret_value.push(fn(ret_value, item))
240 # Three-parameter callbacks additionally receive the index.
241 elif wrapper.params_number == _FnWrapper.THREE:
242 for idx, item in enumerate(sequence):
243 ret_value.push(wrapper(ret_value, item, idx))
244 return ret_value
247def head[T](iterable: Iterable[T]) -> Option[T]:
248 """
249 Returns Some[T] contained 1st item of the given iterable object.
250 If the given iterable is empty, it returns Nothing[T].
252 Args:
253 iterable: an iterable object provided 1st element.
255 Return:
256 Option[T]: Returns Some(item) if non-empty, otherwise Nothing().
257 """
258 # Use a unique sentinel so a legitimate `None` element is not mistaken for "empty".
259 iterator = iter(iterable)
260 sentinel = object()
261 item = next(iterator, sentinel)
262 return Nothing() if item is sentinel else Some(cast(T, item))
265def tail[T](sequence: Sequence[T]) -> Sequence[T]:
266 """
267 Returns Some[T] contained a slice of the given sequence without 1st element,
268 i.g. it returns sequence[1:].
269 If the sequence is empty, this function returns it.
271 Args:
272 sequence: a sequence used to get a slice.
274 Return:
275 Sequence[T]: A slice of the given sequence.
276 """
277 return sequence[1:] if sequence else sequence
280def split_first[T](sequence: Sequence[T]) -> tuple[Option[T], Sequence[T]]:
281 """
282 Splits a sequence into its first element (if any) and the remaining tail.
284 Args:
285 sequence: Sequence to split.
287 Returns:
288 tuple[Option[T], Sequence[T]]: The first element as an Option and the remaining items.
289 """
290 return (Some(sequence[0]), sequence[1:]) if sequence else (Nothing(), sequence)
293def split_last[T](sequence: Sequence[T]) -> tuple[Sequence[T], Option[T]]:
294 """
295 Splits a sequence into its initial part and its last element (if any).
297 Args:
298 sequence: Sequence to split.
300 Returns:
301 tuple[Sequence[T], Option[T]]: The initial items and the last element as an Option.
302 """
303 return (sequence[:-1], Some(sequence[-1])) if sequence else (sequence, Nothing())
306def first[T](iterable: Iterable[T]) -> Option[T]:
307 """
308 Returns Some[T] contained 1st item of the given iterable object.
309 If the given iterable is empty, it returns Nothing[T].
311 Args:
312 iterable: an iterable object provided 1st element.
314 Return:
315 Option[T]: Returns Some(item) if non-empty, otherwise Nothing().
316 """
317 iterator = iter(iterable)
318 sentinel = object()
319 item = next(iterator, sentinel)
320 return Nothing() if item is sentinel else Some(cast(T, item))
323def last[T](sequence: Sequence[T]) -> Option[T]:
324 """
325 Returns Some[Any] contained last item of the given sequence.
326 If the given sequence is empty, it returns Nothing[Any].
328 Args:
329 sequence: a sequence provided last element.
331 Return:
332 Option[T]: Returns Some(item) if non-empty, otherwise Nothing().
333 """
334 return Some(sequence[-1]) if sequence else Nothing()
337def is_empty(iterable: Sized) -> bool:
338 """
339 Checks if the given iterable object is empty.
341 Args:
342 iterable: an iterable object.
344 Return:
345 bool: Returns true if the iterable is empty, otherwise returns false.
346 """
347 return len(iterable) == 0
350def is_not_empty(value: Sized) -> bool:
351 """
352 Checks whether the given object of Sized is not empty.
354 Args:
355 Sized: an sized object.
357 Return:
358 bool: Returns true if the value is not empty, otherwise returns false.
359 """
360 return len(value) != 0
363def index[T](predicate: Callable[..., bool | Option[Any]] | T, sequence: Sequence[T]) -> Option[int]:
364 """
365 Finds the position of 1st occurrence of a specified element in the given iterable object.
366 It returns Some[Any] contained the index of the found element, and if the element is not found, it returns Nothing
368 Args:
369 predicate: a function checking elements or a value used for checking.
370 iterable: an iterable object providing elements for checking.
372 Return:
373 Option[int]: A found index
374 """
375 if is_not_empty(sequence):
376 if callable(predicate):
377 wrapper = _FnWrapper(predicate)
378 for idx, value in enumerate(sequence):
379 if wrapper(value, idx, sequence):
380 return Some(idx)
381 else:
382 expected_value: T = cast(T, predicate)
384 for idx, value in enumerate(sequence):
385 if expected_value == value:
386 return Some(idx)
387 return Nothing()
390def find[T](callback: Callable[[T], Option[T]], sequence: Sequence[T]) -> Option[T]:
391 """
392 Finds a specified element in the given iterable object.
393 It returns Some[Any] contained f of the found element, and if the element is not found, it returns Nothing
395 Args:
396 predicate: a function checking elements or a value used for checking.
397 sequence: a sequence object providing elements for checking.
399 Return:
400 Option[int]: A found index
401 """
402 for item in sequence:
403 ret_value = callback(item)
404 # Accept either a non-empty Option or any truthy value as a match.
405 if isinstance(ret_value, Option) and ret_value.is_some():
406 return ret_value
407 elif ret_value:
408 return Some(item)
410 return Nothing()
413type _FnWrapperType[T, R] = _FnWrapper[T, R] | _FnWrapper2[T, R]
416def _abstract_check[T, bool](
417 *,
418 predicate: _FnWrapperType[T, bool],
419 sequence: Sequence[T],
420 init_value: bool,
421 check: Callable[[bool], bool],
422):
423 ret_value = init_value
424 # Empty input keeps the identity value (False for any_of, True for all_of/none_of).
425 if is_empty(sequence):
426 return ret_value
428 # Stop early when `check` flags the predicate result as decisive.
429 if predicate.params_number == _FnWrapper.ONE:
430 for item in sequence:
431 ret_value = predicate(item)
432 if check(ret_value):
433 break
434 else:
435 for idx, item in enumerate(sequence):
436 ret_value = predicate(item, idx, sequence)
437 if check(ret_value):
438 break
440 return ret_value