@@ -339,6 +339,263 @@ def gauss(self, mu: float = 0.0, sigma: float = 1.0) -> float:
339339 # Scale and shift to desired mean and standard deviation
340340 return mu + sigma * z0
341341
342+ def triangular (
343+ self , low : float = 0.0 , high : float = 1.0 , mode : float | None = None
344+ ) -> float :
345+ """
346+ Generate a random float from the triangular distribution.
347+
348+ The triangular distribution is a continuous probability distribution
349+ with a lower limit, upper limit, and mode (peak).
350+
351+ Args:
352+ low: Lower limit (default: 0.0)
353+ high: Upper limit (default: 1.0)
354+ mode: Peak of the distribution. If None, defaults to midpoint.
355+
356+ Returns:
357+ Random float from the triangular distribution
358+
359+ Raises:
360+ ValueError: If low > high or mode is outside [low, high]
361+ """
362+ import math
363+
364+ if low > high :
365+ raise ValueError (f"triangular: low ({ low } ) must be <= high ({ high } )" )
366+
367+ if mode is None :
368+ mode = (low + high ) / 2.0
369+
370+ if not (low <= mode <= high ):
371+ raise ValueError (
372+ f"triangular: mode ({ mode } ) must be in [{ low } , { high } ]"
373+ )
374+
375+ # Handle degenerate case
376+ if low == high :
377+ return low
378+
379+ u = self .random ()
380+ c = (mode - low ) / (high - low )
381+
382+ if u < c :
383+ return low + math .sqrt (u * (high - low ) * (mode - low ))
384+ else :
385+ return high - math .sqrt ((1 - u ) * (high - low ) * (high - mode ))
386+
387+ def exponential (self , lambd : float = 1.0 ) -> float :
388+ """
389+ Generate a random float from the exponential distribution.
390+
391+ The exponential distribution describes time between events in a
392+ Poisson process. It's commonly used to model waiting times.
393+
394+ Args:
395+ lambd: Rate parameter (1/mean). Must be positive.
396+ Note: Named 'lambd' to avoid conflict with Python keyword.
397+
398+ Returns:
399+ Random float from Exp(lambda)
400+
401+ Raises:
402+ ValueError: If lambd <= 0
403+ """
404+ import math
405+
406+ if lambd <= 0 :
407+ raise ValueError (f"exponential: lambd ({ lambd } ) must be positive" )
408+
409+ # Inverse transform sampling
410+ # If U ~ Uniform(0,1), then -ln(U)/lambda ~ Exp(lambda)
411+ u = self .random ()
412+ while u == 0 : # Avoid log(0)
413+ u = self .random ()
414+
415+ return - math .log (u ) / lambd
416+
417+ def weighted_choice (self , seq : Sequence [T ], weights : Sequence [float ]) -> T :
418+ """
419+ Return a random element from a sequence with weighted probabilities.
420+
421+ Elements with higher weights are more likely to be selected.
422+
423+ Args:
424+ seq: A non-empty sequence
425+ weights: Weights for each element (must be same length as seq)
426+
427+ Returns:
428+ A randomly selected element
429+
430+ Raises:
431+ ValueError: If seq and weights have different lengths
432+ ValueError: If any weight is negative
433+ ValueError: If all weights are zero
434+ IndexError: If the sequence is empty
435+
436+ Example:
437+ >>> tap.weighted_choice(['rare', 'common', 'common'], [1, 10, 10])
438+ 'common' # Most likely
439+ """
440+ if not seq :
441+ raise IndexError ("Cannot choose from an empty sequence" )
442+
443+ if len (seq ) != len (weights ):
444+ raise ValueError (
445+ f"weighted_choice: seq length ({ len (seq )} ) != "
446+ f"weights length ({ len (weights )} )"
447+ )
448+
449+ if any (w < 0 for w in weights ):
450+ raise ValueError ("weighted_choice: weights must be non-negative" )
451+
452+ total = sum (weights )
453+ if total == 0 :
454+ raise ValueError ("weighted_choice: at least one weight must be > 0" )
455+
456+ # Generate a random threshold in [0, total)
457+ threshold = self .random () * total
458+
459+ # Find which "bucket" the threshold falls into
460+ cumulative = 0.0
461+ for i , weight in enumerate (weights ):
462+ cumulative += weight
463+ if threshold < cumulative :
464+ return seq [i ]
465+
466+ # Fallback (shouldn't happen, but handles floating point edge cases)
467+ return seq [- 1 ]
468+
469+ def random_uuid (self ) -> str :
470+ """
471+ Generate a random UUID (version 4).
472+
473+ UUID v4 uses random numbers for all significant bits,
474+ making it ideal for unique identifiers.
475+
476+ Returns:
477+ A UUID string in the format 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
478+ where x is a random hex digit and y is one of 8, 9, a, or b.
479+
480+ Example:
481+ >>> tap.random_uuid()
482+ 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
483+ """
484+ # Generate 16 random bytes
485+ raw = self .randbytes (16 )
486+ raw_list = list (raw )
487+
488+ # Set version to 4 (0100 in high nibble of byte 6)
489+ raw_list [6 ] = (raw_list [6 ] & 0x0F ) | 0x40
490+
491+ # Set variant to RFC 4122 (10xx in high bits of byte 8)
492+ raw_list [8 ] = (raw_list [8 ] & 0x3F ) | 0x80
493+
494+ # Convert to hex string
495+ hex_str = bytes (raw_list ).hex ()
496+
497+ # Format as UUID: 8-4-4-4-12
498+ return (
499+ f"{ hex_str [0 :8 ]} -{ hex_str [8 :12 ]} -{ hex_str [12 :16 ]} -"
500+ f"{ hex_str [16 :20 ]} -{ hex_str [20 :32 ]} "
501+ )
502+
503+ def random_token (self , length : int = 32 , encoding : str = "hex" ) -> str :
504+ """
505+ Generate a random token string.
506+
507+ Useful for API keys, session tokens, CSRF tokens, etc.
508+
509+ Args:
510+ length: Number of random bytes to use (default: 32)
511+ encoding: Output encoding - 'hex' or 'base64' (default: 'hex')
512+
513+ Returns:
514+ A random token string
515+
516+ Raises:
517+ ValueError: If length <= 0 or encoding is invalid
518+
519+ Example:
520+ >>> tap.random_token(16, 'hex')
521+ 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6'
522+ >>> tap.random_token(16, 'base64')
523+ 'obLNGb5hFbJQ9Q4='
524+ """
525+ if length <= 0 :
526+ raise ValueError (f"random_token: length ({ length } ) must be positive" )
527+
528+ raw = self .randbytes (length )
529+
530+ if encoding == "hex" :
531+ return raw .hex ()
532+ elif encoding == "base64" :
533+ import base64
534+ return base64 .urlsafe_b64encode (raw ).decode ("ascii" )
535+ else :
536+ raise ValueError (
537+ f"random_token: encoding must be 'hex' or 'base64', "
538+ f"got '{ encoding } '"
539+ )
540+
541+ def random_password (
542+ self ,
543+ length : int = 16 ,
544+ charset : str | None = None ,
545+ include_uppercase : bool = True ,
546+ include_lowercase : bool = True ,
547+ include_digits : bool = True ,
548+ include_symbols : bool = True
549+ ) -> str :
550+ """
551+ Generate a secure random password.
552+
553+ Args:
554+ length: Password length (default: 16)
555+ charset: Custom character set. If provided, overrides include_* flags.
556+ include_uppercase: Include A-Z (default: True)
557+ include_lowercase: Include a-z (default: True)
558+ include_digits: Include 0-9 (default: True)
559+ include_symbols: Include !@#$%^&*()_+-= (default: True)
560+
561+ Returns:
562+ A random password string
563+
564+ Raises:
565+ ValueError: If length <= 0 or no characters available
566+
567+ Example:
568+ >>> tap.random_password(12)
569+ 'Kx9#mP2$nL7@'
570+ >>> tap.random_password(8, charset='abc123')
571+ '2ab1c3a1'
572+ """
573+ if length <= 0 :
574+ raise ValueError (f"random_password: length ({ length } ) must be positive" )
575+
576+ if charset is not None :
577+ if not charset :
578+ raise ValueError ("random_password: charset cannot be empty" )
579+ chars = charset
580+ else :
581+ chars = ""
582+ if include_uppercase :
583+ chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
584+ if include_lowercase :
585+ chars += "abcdefghijklmnopqrstuvwxyz"
586+ if include_digits :
587+ chars += "0123456789"
588+ if include_symbols :
589+ chars += "!@#$%^&*()_+-="
590+
591+ if not chars :
592+ raise ValueError (
593+ "random_password: at least one character type must be included"
594+ )
595+
596+ # Generate password by choosing random characters
597+ return "" .join (self .choice (chars ) for _ in range (length ))
598+
342599 # -------------------------------------------------------------------------
343600 # String Representation
344601 # -------------------------------------------------------------------------
0 commit comments