pydasa 0.4.7__py3-none-any.whl

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 (58) hide show
  1. pydasa/__init__.py +103 -0
  2. pydasa/_version.py +6 -0
  3. pydasa/analysis/__init__.py +0 -0
  4. pydasa/analysis/scenario.py +584 -0
  5. pydasa/analysis/simulation.py +1158 -0
  6. pydasa/context/__init__.py +0 -0
  7. pydasa/context/conversion.py +11 -0
  8. pydasa/context/system.py +17 -0
  9. pydasa/context/units.py +15 -0
  10. pydasa/core/__init__.py +15 -0
  11. pydasa/core/basic.py +287 -0
  12. pydasa/core/cfg/default.json +136 -0
  13. pydasa/core/constants.py +27 -0
  14. pydasa/core/io.py +102 -0
  15. pydasa/core/setup.py +269 -0
  16. pydasa/dimensional/__init__.py +0 -0
  17. pydasa/dimensional/buckingham.py +728 -0
  18. pydasa/dimensional/fundamental.py +146 -0
  19. pydasa/dimensional/model.py +1077 -0
  20. pydasa/dimensional/vaschy.py +633 -0
  21. pydasa/elements/__init__.py +19 -0
  22. pydasa/elements/parameter.py +218 -0
  23. pydasa/elements/specs/__init__.py +22 -0
  24. pydasa/elements/specs/conceptual.py +161 -0
  25. pydasa/elements/specs/numerical.py +469 -0
  26. pydasa/elements/specs/statistical.py +229 -0
  27. pydasa/elements/specs/symbolic.py +394 -0
  28. pydasa/serialization/__init__.py +27 -0
  29. pydasa/serialization/parser.py +133 -0
  30. pydasa/structs/__init__.py +0 -0
  31. pydasa/structs/lists/__init__.py +0 -0
  32. pydasa/structs/lists/arlt.py +578 -0
  33. pydasa/structs/lists/dllt.py +18 -0
  34. pydasa/structs/lists/ndlt.py +262 -0
  35. pydasa/structs/lists/sllt.py +746 -0
  36. pydasa/structs/tables/__init__.py +0 -0
  37. pydasa/structs/tables/htme.py +182 -0
  38. pydasa/structs/tables/scht.py +774 -0
  39. pydasa/structs/tools/__init__.py +0 -0
  40. pydasa/structs/tools/hashing.py +53 -0
  41. pydasa/structs/tools/math.py +149 -0
  42. pydasa/structs/tools/memory.py +54 -0
  43. pydasa/structs/types/__init__.py +0 -0
  44. pydasa/structs/types/functions.py +131 -0
  45. pydasa/structs/types/generics.py +54 -0
  46. pydasa/validations/__init__.py +0 -0
  47. pydasa/validations/decorators.py +510 -0
  48. pydasa/validations/error.py +100 -0
  49. pydasa/validations/patterns.py +32 -0
  50. pydasa/workflows/__init__.py +1 -0
  51. pydasa/workflows/influence.py +497 -0
  52. pydasa/workflows/phenomena.py +529 -0
  53. pydasa/workflows/practical.py +765 -0
  54. pydasa-0.4.7.dist-info/METADATA +320 -0
  55. pydasa-0.4.7.dist-info/RECORD +58 -0
  56. pydasa-0.4.7.dist-info/WHEEL +5 -0
  57. pydasa-0.4.7.dist-info/licenses/LICENSE +674 -0
  58. pydasa-0.4.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,746 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Module sllt.py
4
+ ===========================================
5
+
6
+ Module for the custom **SingleLinkedList** data structure in *PyDASA*. Essential for Dimensional Analysis and Data Science operations.
7
+
8
+ Classes:
9
+ **SingleLinkedList**: Implements a single linked list with methods for insertion, deletion, and traversal.
10
+
11
+ *IMPORTANT:* based on the implementations proposed by the following authors/books:
12
+
13
+ #. Algorithms, 4th Edition, Robert Sedgewick and Kevin Wayne.
14
+ #. Data Structure and Algorithms in Python, M.T. Goodrich, R. Tamassia, M.H. Goldwasser.
15
+ """
16
+
17
+ # native python modules
18
+ # dataclass imports
19
+ from dataclasses import dataclass
20
+ # data type imports
21
+ from typing import List, Optional, Callable, Generic, Iterator, Any
22
+ # code inspection imports
23
+ import inspect
24
+
25
+ # custom modules
26
+ # linked list node implementation
27
+ from pydasa.structs.lists.ndlt import SLNode
28
+
29
+ # generic types and global variables
30
+ from pydasa.structs.types.generics import T
31
+ from pydasa.structs.types.generics import DFLT_DICT_KEY
32
+ from pydasa.structs.types.generics import VLD_IOTYPE_LT
33
+ from pydasa.structs.types.functions import dflt_cmp_function_lt
34
+ # generic error handling and type checking
35
+ from pydasa.validations.error import handle_error as error
36
+
37
+ # checking custom modules
38
+ assert T
39
+ assert DFLT_DICT_KEY
40
+ assert VLD_IOTYPE_LT
41
+ assert dflt_cmp_function_lt
42
+ assert error
43
+
44
+
45
+ @dataclass
46
+ class SingleLinkedList(Generic[T]):
47
+ """**SingleLinkedList** implements a single linked list data structure for PyDASA.
48
+
49
+ Args:
50
+ Generic (T): Generic type for a Python data structure.
51
+
52
+ Returns:
53
+ SingleLinkedList: a generic single linked list data structure with the following attributes:
54
+ - **cmp_function**: function to compare elements in the list.
55
+ - **key**: key to identify the elements in the list.
56
+ - **first**: reference to the first node of the list.
57
+ - **last**: reference to the last node of the list.
58
+ - **_size**: size of the list.
59
+ """
60
+
61
+ # the cmp_function is used to compare elements, not defined by default
62
+ # :attr: cmp_function
63
+ cmp_function: Optional[Callable[[T, T], int]] = None
64
+ """
65
+ Customizable comparison function for *SingleLinkedList* elements. Defaults to *dflt_cmp_function_lt()* from *PyDASA*, but can be overridden by the user.
66
+ """
67
+
68
+ # reference to the first node of the list
69
+ # :attr: _first
70
+ _first: Optional[SLNode[T]] = None
71
+ """
72
+ Reference to the first node of the *SingleLinkedList*.
73
+ """
74
+
75
+ # reference to the last node of the list
76
+ # :attr: _last
77
+ _last: Optional[SLNode[T]] = None
78
+ """
79
+ Reference to the last node of the *SingleLinkedList*.
80
+ """
81
+
82
+ # the key is used to compare elements, not defined by default
83
+ # :attr: key
84
+ key: Optional[str] = DFLT_DICT_KEY
85
+ """
86
+ Customizable key name for identifying elements in the *SingleLinkedList*. Defaults to *DFLT_DICT_KEY = '_id'* from *PyDASA*, but can be overridden by the user.
87
+ """
88
+
89
+ # by default, the list is empty
90
+ # :attr: _size
91
+ _size: int = 0
92
+ """
93
+ Size of the *SingleLinkedList*, starting at 0 and updated with each modification.
94
+ """
95
+
96
+ # input elements from python list
97
+ # :attr: iodata
98
+ iodata: Optional[List[T]] = None
99
+ """
100
+ Optional Python list for loading external data intho the *SingleLinkedList*. Defaults to *None* but can be provided during creation.
101
+ """
102
+
103
+ def __post_init__(self) -> None:
104
+ """*__post_init__()* Initializes the *SingleLinkedList* after creation by setting attributes like *cmp_function*, *key*, *first*, *last*, and *iodata*.
105
+
106
+ *NOTE:* Special method called automatically after object creation.
107
+ """
108
+ try:
109
+ # if the key is not defined, use the default
110
+ if self.key is None:
111
+ self.key = DFLT_DICT_KEY
112
+
113
+ # if the compare function is not defined, use the default
114
+ if self.cmp_function is None:
115
+ self.cmp_function = self.default_compare
116
+
117
+ # if elements are provided, add them to the ArrayList
118
+ if self.iodata is not None:
119
+ if not isinstance(self.iodata, VLD_IOTYPE_LT):
120
+ raise TypeError(f"iodata must be a valid iterable type, got {type(self.iodata)}")
121
+
122
+ for elm in self.iodata:
123
+ self.append(elm)
124
+
125
+ # Clear iodata after processing
126
+ self.iodata = None
127
+
128
+ except Exception as err:
129
+ self._error_handler(err)
130
+
131
+ def default_compare(self, elm1: Any, elm2: Any) -> int:
132
+ """*default_compare()* Default comparison function for *SingleLinkedList* elements. Compares two elements and returns:
133
+ - 0 if they are equal,
134
+ - 1 if the first is greater,
135
+ - -1 if the first is smaller.
136
+
137
+ Args:
138
+ elm1 (Any): First element to compare.
139
+ elm2 (Any): Second element to compare.
140
+
141
+ Returns:
142
+ int: Comparison result.
143
+ """
144
+ try:
145
+ # default comparison needs the key to be defined
146
+ if self.key is None:
147
+ raise ValueError("Key must be set before comparison")
148
+ return dflt_cmp_function_lt(elm1, elm2, self.key)
149
+ except Exception as err:
150
+ self._error_handler(err)
151
+ raise # Re-raise the exception after handling
152
+
153
+ @property
154
+ def size(self) -> int:
155
+ """*size()* Property to retrieve the number of elements in the *SingleLinkedList*.
156
+
157
+ Returns:
158
+ int: number of elements in the *SingleLinkedList*.
159
+ """
160
+ return self._size
161
+
162
+ @property
163
+ def empty(self) -> bool:
164
+ """*empty()* Property to check if the *SingleLinkedList* is empty.
165
+
166
+ Returns:
167
+ bool: True if the *SingleLinkedList* is empty, False otherwise.
168
+ """
169
+ return self._size == 0
170
+
171
+ def clear(self) -> None:
172
+ """*clear()* clears the *SingleLinkedList* by removing all elements and resetting the size to 0.
173
+
174
+ NOTE: This method is used to empty the *SingleLinkedList* without deleting the object itself.
175
+ """
176
+ self._first = None
177
+ self._last = None
178
+ self._size = 0
179
+
180
+ def prepend(self, elm: T) -> None:
181
+ """*prepend()* adds an element to the beginning of the *SingleLinkedList*.
182
+
183
+ Args:
184
+ elm (T): element to be added to the beginning of the structure.
185
+ """
186
+ # if the element type is valid, add it to the list
187
+ if self._validate_type(elm):
188
+ # create a new node
189
+ _new = SLNode(elm)
190
+ _new.next = self._first
191
+ self._first = _new
192
+ if self.size == 0:
193
+ self._last = self._first
194
+ self._size += 1
195
+
196
+ def append(self, elm: T) -> None:
197
+ """*append()* adds an element to the end of the *SingleLinkedList*.
198
+
199
+ Args:
200
+ elm (T): element to be added to the end of the structure.
201
+ """
202
+ # if the element type is valid, add it to the list
203
+ if self._validate_type(elm):
204
+ # create a new node
205
+ _new = SLNode(elm)
206
+ if self._last is None:
207
+ self._first = _new
208
+ else:
209
+ self._last.next = _new
210
+ self._last = _new
211
+ self._size += 1
212
+
213
+ def insert(self, elm: T, pos: int) -> None:
214
+ """*insert()* adds an element to the *SingleLinkedList* at a specific position.
215
+
216
+ Args:
217
+ elm (T): element to be added to the structure.
218
+ pos (int): position where the element will be added.
219
+
220
+ Raises:
221
+ IndexError: error if the structure is empty.
222
+ IndexError: error if the position is invalid.
223
+ TypeError: error if the element type is invalid.
224
+ """
225
+ if not self.empty and self._validate_type(elm):
226
+ if pos < 0 or pos > self.size:
227
+ raise IndexError("Position is out of range")
228
+
229
+ # create a new node
230
+ _new = SLNode(elm)
231
+
232
+ # if the position is the first, add it to the first
233
+ if pos == 0:
234
+ _new.next = self._first
235
+ self._first = _new
236
+ if self._last is None: # Empty list case
237
+ self._last = _new
238
+
239
+ # if the position is the last (append), add it to the end
240
+ elif pos == self.size:
241
+ if self._last is not None:
242
+ self._last.next = _new
243
+ self._last = _new
244
+
245
+ # otherwise, insert in the middle
246
+ else:
247
+ i = 0
248
+ _cur = self._first
249
+ _prev: Optional[SLNode[T]] = None
250
+ while i < pos and _cur is not None:
251
+ _prev = _cur
252
+ _cur = _cur.next
253
+ i += 1
254
+ if _prev is not None:
255
+ _new.next = _cur
256
+ _prev.next = _new
257
+ # increment the size
258
+ self._size += 1
259
+ else:
260
+ raise IndexError("Empty data structure")
261
+
262
+ @property
263
+ def first(self) -> T:
264
+ """*first* Property to read the first element of the *SingleLinkedList*.
265
+
266
+ Raises:
267
+ IndexError: error if the structure is empty.
268
+
269
+ Returns:
270
+ T: the first element of the *SingleLinkedList*.
271
+ """
272
+ if self.empty or self._first is None:
273
+ raise IndexError("Empty data structure")
274
+ return self._first.data
275
+
276
+ @property
277
+ def last(self) -> T:
278
+ """*last* Property to read the last element of the *SingleLinkedList*.
279
+
280
+ Raises:
281
+ Exception: error if the structure is empty.
282
+
283
+ Returns:
284
+ T: the last element of the *SingleLinkedList*.
285
+ """
286
+ if self.empty or self._last is None:
287
+ raise IndexError("Empty data structure")
288
+ return self._last.data
289
+
290
+ def get(self, pos: int) -> T:
291
+ """*get()* retrieves an element from the *SingleLinkedList* at a specific position.
292
+
293
+ Args:
294
+ pos (int): position of the element to be retrieved.
295
+
296
+ Raises:
297
+ IndexError: error if the structure is empty.
298
+ IndexError: error if the position is invalid.
299
+
300
+ Returns:
301
+ T: the element at the specified position in the *SingleLinkedList*.
302
+ """
303
+ if self.empty:
304
+ raise IndexError("Empty data structure")
305
+ if pos < 0 or pos > self.size - 1:
306
+ raise IndexError(f"Index {pos} is out of range")
307
+
308
+ # current node starting at the first node
309
+ _cur = self._first
310
+ i = 0
311
+ # iterate to the desired position
312
+ while i != pos and _cur is not None:
313
+ _cur = _cur.next
314
+ i += 1
315
+
316
+ if _cur is None:
317
+ raise IndexError(f"Corrupted list structure at position {pos}")
318
+
319
+ return _cur.data
320
+
321
+ def __getitem__(self, pos: int) -> Optional[T]:
322
+ """*__getitem__()* retrieves an element from the *SingleLinkedList* at a specific position.
323
+
324
+ NOTE: This method is used to access the elements of the *SingleLinkedList* using the square brackets notation.
325
+
326
+ Args:
327
+ pos (int): position of the element to be retrieved.
328
+
329
+ Raises:
330
+ IndexError: error if the structure is empty.
331
+ IndexError: error if the position is invalid.
332
+
333
+ Returns:
334
+ Optional[T]: the element at the specified position in the *SingleLinkedList*.
335
+ """
336
+ return self.get(pos)
337
+
338
+ def pop_first(self) -> T:
339
+ """*pop_first()* removes the first element from the *SingleLinkedList*.
340
+
341
+ Raises:
342
+ IndexError: error if the structure is empty.
343
+
344
+ Returns:
345
+ T: the first element of the *SingleLinkedList*.
346
+ """
347
+ # check if the list is empty
348
+ if self.empty or self._first is None:
349
+ raise IndexError("Empty data structure")
350
+
351
+ # save the data before removing the node
352
+ _data = self._first.data
353
+
354
+ # move first pointer to the next node
355
+ self._first = self._first.next
356
+ self._size -= 1
357
+
358
+ # if the list is now empty, set last to None
359
+ if self._first is None:
360
+ self._last = None
361
+
362
+ return _data
363
+
364
+ def pop_last(self) -> T:
365
+ """*pop_last()* removes the last element from the *SingleLinkedList*.
366
+
367
+ Raises:
368
+ IndexError: error if the structure is empty.
369
+
370
+ Returns:
371
+ T: the last element of the *SingleLinkedList*.
372
+ """
373
+ # Check if the list is empty
374
+ if self.empty or self._last is None:
375
+ raise IndexError("Empty data structure")
376
+
377
+ # Save the data before removing the node
378
+ _data = self._last.data
379
+
380
+ # if the list has only one element, set the first and last to None
381
+ if self._first == self._last:
382
+ self._first = None
383
+ self._last = None
384
+
385
+ # otherwise, remove the last element
386
+ else:
387
+ _cur = self._first
388
+ # traverse the list to find the second-to-last element
389
+ while _cur is not None and _cur.next != self._last:
390
+ _cur = _cur.next
391
+
392
+ # Ensure we found the second-to-last node
393
+ if _cur is None:
394
+ raise IndexError("Corrupted list structure")
395
+
396
+ # rearrange the last element
397
+ self._last = _cur
398
+ self._last.next = None
399
+
400
+ self._size -= 1
401
+ return _data
402
+
403
+ def remove(self, pos: int) -> T:
404
+ """*remove()* removes an element from the *SingleLinkedList* at a specific position.
405
+
406
+ Args:
407
+ pos (int): position of the element to be removed.
408
+
409
+ Raises:
410
+ IndexError: error if the structure is empty.
411
+ IndexError: error if the position is invalid.
412
+
413
+ Returns:
414
+ T: the element removed from the *SingleLinkedList*.
415
+ """
416
+ # check if the list is empty
417
+ if self.empty or self._first is None:
418
+ raise IndexError("Empty data structure")
419
+ # check if the position is valid
420
+ if pos < 0 or pos > self.size - 1:
421
+ raise IndexError(f"Index {pos} is out of range")
422
+
423
+ # if removing the first element
424
+ if pos == 0:
425
+ _data = self._first.data
426
+ self._first = self._first.next
427
+ # if list is now empty, update last
428
+ if self._first is None:
429
+ self._last = None
430
+ # if removing from middle or end
431
+ else:
432
+ _cur = self._first
433
+ _prev = self._first
434
+ i = 0
435
+ # traverse to the position
436
+ while i != pos and _cur is not None:
437
+ _prev = _cur
438
+ _cur = _cur.next
439
+ i += 1
440
+
441
+ # Check if we found the node
442
+ if _cur is None:
443
+ raise IndexError(f"Corrupted list structure at position {pos}")
444
+
445
+ _data = _cur.data
446
+ _prev.next = _cur.next
447
+
448
+ # if removing the last element, update last pointer
449
+ if _cur == self._last:
450
+ self._last = _prev
451
+
452
+ self._size -= 1
453
+ return _data
454
+
455
+ def compare(self, elem1: T, elem2: T) -> int:
456
+ """*compare()* compares two elements using the comparison function defined in the *SingleLinkedList*.
457
+
458
+ Args:
459
+ elem1 (T): first element to compare.
460
+ elem2 (T): second element to compare.
461
+
462
+ Raises:
463
+ TypeError: error if the *cmp_function* is not defined.
464
+
465
+ Returns:
466
+ int: -1 if elem1 < elem2, 0 if elem1 == elem2, 1 if elem1 > elem2.
467
+ """
468
+ if self.cmp_function is None:
469
+ # raise an exception if the cmp function is not defined
470
+ raise TypeError("Undefined compare function!!!")
471
+ # use the structure cmp function
472
+ return self.cmp_function(elem1, elem2)
473
+
474
+ def index_of(self, elm: T) -> int:
475
+ """*index_of()* searches for the first occurrence of an element in the *SingleLinkedList*. If the element is found, it returns its index; otherwise, it returns -1.
476
+
477
+ Args:
478
+ elm (T): element to search for in the *SingleLinkedList*.
479
+
480
+ Returns:
481
+ int: index of the element in the *SingleLinkedList* or -1 if not found.
482
+ """
483
+ if self.empty:
484
+ raise IndexError("Empty data structure")
485
+
486
+ _idx = -1
487
+ _node = self._first
488
+ found = False
489
+ i = 0
490
+
491
+ # iterate through the list to find the element
492
+ while not found and _node is not None and i < self.size:
493
+ # using the structure cmp function
494
+ if self.compare(elm, _node.data) == 0:
495
+ found = True
496
+ _idx = i
497
+ else:
498
+ _node = _node.next
499
+ i += 1
500
+
501
+ return _idx
502
+
503
+ def update(self, new_data: T, pos: int) -> None:
504
+ """*update()* updates an element in the *SingleLinkedList* at a specific position.
505
+
506
+ Args:
507
+ new_data (T): new data to be updated in the structure.
508
+ pos (int): position of the element to be updated.
509
+
510
+ Raises:
511
+ IndexError: error if the structure is empty.
512
+ IndexError: error if the position is invalid.
513
+ """
514
+ if self.empty:
515
+ raise IndexError("Empty data structure")
516
+ elif pos < 0 or pos > self.size - 1:
517
+ raise IndexError(f"Index {pos} is out of range")
518
+ # if the element type is valid, update the element
519
+ elif self._validate_type(new_data):
520
+ _cur = self._first
521
+ i = 0
522
+ while i != pos and _cur is not None:
523
+ _cur = _cur.next
524
+ i += 1
525
+
526
+ if _cur is None:
527
+ raise IndexError(f"Corrupted list structure at position {pos}")
528
+
529
+ _cur.data = new_data
530
+
531
+ def swap(self, pos1: int, pos2: int) -> None:
532
+ """*swap()* swaps two elements in the *SingleLinkedList* at specific positions.
533
+
534
+
535
+ Args:
536
+ pos1 (int): position of the first element to swap.
537
+ pos2 (int): position of the second element to swap.
538
+
539
+ Raises:
540
+ IndexError: error if the structure is empty.
541
+ IndexError: error if the first position is invalid.
542
+ IndexError: error if the second position is invalid.
543
+ """
544
+ if self.empty:
545
+ raise IndexError("Empty data structure")
546
+ elif pos1 < 0 or pos1 > self.size - 1:
547
+ raise IndexError("Index", pos1, "is out of range")
548
+ elif pos2 < 0 or pos2 > self.size - 1:
549
+ raise IndexError("Index", pos2, "is out of range")
550
+ info_pos1 = self.get(pos1)
551
+ info_pos2 = self.get(pos2)
552
+ self.update(info_pos2, pos1)
553
+ self.update(info_pos1, pos2)
554
+
555
+ def sublist(self, start: int, end: int) -> "SingleLinkedList[T]":
556
+ """*sublist()* creates a new *SingleLinkedList* containing a sublist of elements from the original *SingleLinkedList*. The sublist is defined by the start and end indices.
557
+
558
+ NOTE: The start index is inclusive, and the end index is inclusive.
559
+
560
+ Args:
561
+ start (int): start index of the sublist.
562
+ end (int): end index of the sublist.
563
+
564
+ Raises:
565
+ IndexError: error if the structure is empty.
566
+ IndexError: error if the start or end index are invalid.
567
+
568
+ Returns:
569
+ SingleLinkedList[T]: a new *SingleLinkedList* containing the sublist of elements.
570
+ """
571
+ if self.empty:
572
+ raise IndexError("Empty data structure")
573
+ elif start < 0 or end > self.size - 1 or start > end:
574
+ raise IndexError(f"Invalid range: between [{start}, {end}]")
575
+
576
+ sub_lt = SingleLinkedList(cmp_function=self.cmp_function,
577
+ key=self.key)
578
+ i = 0
579
+ _cur = self._first
580
+
581
+ while i != end + 1 and _cur is not None:
582
+ if i >= start:
583
+ sub_lt.append(_cur.data)
584
+ _cur = _cur.next
585
+ i += 1
586
+ return sub_lt
587
+
588
+ def concat(self, other: "SingleLinkedList[T]") -> "SingleLinkedList[T]":
589
+ """*concat()* concatenates two *SingleLinkedList* objects. The elements of the second list are added to the end of the first list.
590
+
591
+ NOTE: The *cmp_function* and *key* attributes of the two lists must be the same.
592
+
593
+ Args:
594
+ other (SingleLinkedList[T]): the second *SingleLinkedList* to be concatenated.
595
+
596
+ Raises:
597
+ TypeError: error if the *other* argument is not an *SingleLinkedList*.
598
+ TypeError: error if the *key* attributes are not the same.
599
+ TypeError: error if the *cmp_function* are not the same.
600
+
601
+ Returns:
602
+ SingleLinkedList[T]: the concatenated *SingleLinkedList* in the first list.
603
+ """
604
+ if not isinstance(other, SingleLinkedList):
605
+ _msg = f"Structure is not an SingleLinkedList: {type(other)}"
606
+ raise TypeError(_msg)
607
+ if self.key != other.key:
608
+ raise TypeError(f"Invalid key: {self.key} != {other.key}")
609
+ # checking functional code of the cmp function
610
+ if self.cmp_function is not None and other.cmp_function is not None:
611
+ code1 = self.cmp_function.__code__.co_code
612
+ code2 = other.cmp_function.__code__.co_code
613
+ if code1 != code2:
614
+ _msg = f"Invalid compare function: {self.cmp_function}"
615
+ _msg += f" != {other.cmp_function}"
616
+ raise TypeError(_msg)
617
+
618
+ # Handle empty lists
619
+ if other.empty:
620
+ return self # Nothing to concatenate
621
+
622
+ # If self is empty, just copy other's strucure
623
+ if self.empty:
624
+ self._first = other._first
625
+ self._last = other._last
626
+
627
+ # concatenate the two lists
628
+ elif self._last is not None:
629
+ self._last.next = other._first
630
+ self._last = other._last
631
+
632
+ # update the size
633
+ self._size = self.size + other.size
634
+ return self
635
+
636
+ def clone(self) -> "SingleLinkedList[T]":
637
+ """*clone()* creates a copy of the *SingleLinkedList*. The new list is independent of the original list.
638
+
639
+ NOTE: The elements of the new list are the same as the original list, but they are not references to the same objects.
640
+
641
+ Returns:
642
+ SingleLinkedList[T]: a new *SingleLinkedList* with the same elements as the original list.
643
+ """
644
+ # create new list
645
+ copy_lt = SingleLinkedList(cmp_function=self.cmp_function,
646
+ key=self.key)
647
+ # get the first node of the original list
648
+ _cur = self._first
649
+ # traverse the list and add the elements to the new list
650
+ while _cur is not None:
651
+ copy_lt.append(_cur.data)
652
+ _cur = _cur.next
653
+ return copy_lt
654
+
655
+ def _error_handler(self, err: Exception) -> None:
656
+ """*_error_handler()* to process the context (package/class), function name (method), and the error (exception) that was raised to format a detailed error message and traceback.
657
+
658
+ Args:
659
+ err (Exception): Python raised exception.
660
+ """
661
+ _context = self.__class__.__name__
662
+ _function_name = "unknown"
663
+ frame = inspect.currentframe()
664
+ if frame is not None:
665
+ if frame.f_back is not None:
666
+ _function_name = frame.f_back.f_code.co_name
667
+ else:
668
+ _function_name = "unknown"
669
+ error(_context, _function_name, err)
670
+
671
+ def _validate_type(self, elm: T) -> bool:
672
+ """*_validate_type()* checks if the type of the element is valid. If the structure is empty, the type is valid. If the structure is not empty, the type must be the same as the first element in the list.
673
+ This is used to check the type of the element before adding it to the list.
674
+
675
+ Args:
676
+ elm (T): element to be added to the structure.
677
+
678
+ Raises:
679
+ TypeError: error if the type of the element is not valid.
680
+
681
+ Returns:
682
+ bool: True if the type is valid, False otherwise.
683
+ """
684
+ # if the structure is not empty, check the first element type
685
+ if not self.empty and self._first is not None:
686
+ # raise an exception if the type is not valid at the first element
687
+ if not isinstance(elm, type(self._first.data)):
688
+ _msg = f"Invalid data type: {type(elm)} "
689
+ _msg += f"!= {type(self._first.data)}"
690
+ raise TypeError(_msg)
691
+ # otherwise, any type is valid
692
+ return True
693
+
694
+ def __iter__(self) -> Iterator[T]:
695
+ """*__iter__()* to iterate over the elements of the *SingleLinkedList*. This method returns an iterator object that can be used to iterate over the elements of the list.
696
+
697
+ NOTE: This is used to iterate over the nodes of the list using a for loop.
698
+
699
+ Returns:
700
+ Iterator[T]: an iterator object that can be used to iterate over the nodes of the list.
701
+ """
702
+ try:
703
+ # TODO do I need the try/except block?
704
+ _cur = self._first
705
+ while _cur is not None:
706
+ yield _cur.data
707
+ _cur = _cur.next
708
+ except Exception as err:
709
+ self._error_handler(err)
710
+
711
+ def __len__(self) -> int:
712
+ """*__len__()* to get the number of elements in the *SingleLinkedList*. This method returns the size of the list.
713
+
714
+ Returns:
715
+ int: the number of elements in the *SingleLinkedList*.
716
+ """
717
+ return self._size
718
+
719
+ def __str__(self) -> str:
720
+ """*__str__()* to get the string representation of the *SingleLinkedList*. This method returns a string with the elements of the list separated by commas.
721
+
722
+ Returns:
723
+ str: string representation of the *SingleLinkedList*.
724
+ """
725
+ _attr_lt = []
726
+ for attr, value in vars(self).items():
727
+ # Skip private attributes starting with "__"
728
+ if attr.startswith("__"):
729
+ continue
730
+ # Format callable attributes
731
+ if callable(value):
732
+ value = f"{value.__name__}{inspect.signature(value)}"
733
+ # Format attribute name and value
734
+ _attr_name = attr.lstrip("_")
735
+ _attr_lt.append(f"{_attr_name}={repr(value)}")
736
+ # format the string with the SingleLinkedList class name and the attributes
737
+ _str = f"{self.__class__.__name__}({', '.join(_attr_lt)})"
738
+ return _str
739
+
740
+ def __repr__(self) -> str:
741
+ """*__repr__()* get the string representation of the *SingleLinkedList*. This method returns a string representation,
742
+
743
+ Returns:
744
+ str: string representation of the *SingleLinkedList*.
745
+ """
746
+ return self.__str__()