76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""
|
|
©AngelaMos | 2026
|
|
analyzer.py
|
|
|
|
Statistical frequency analysis for ranking Caesar cipher brute-force results
|
|
|
|
Provides the FrequencyAnalyzer class that scores decryption candidates
|
|
by comparing their letter distributions against expected English frequency
|
|
percentages using a chi-squared test. Lower scores indicate text that
|
|
more closely matches natural English.
|
|
|
|
Connects to:
|
|
constants.py - imports ENGLISH_LETTER_FREQUENCIES
|
|
main.py - crack command passes CaesarCipher.crack() output to rank_candidates()
|
|
"""
|
|
|
|
from collections import Counter
|
|
|
|
from caesar_cipher.constants import ENGLISH_LETTER_FREQUENCIES
|
|
|
|
|
|
class FrequencyAnalyzer:
|
|
"""
|
|
Analyzes text for English language patterns using letter frequency distribution
|
|
"""
|
|
def __init__(self) -> None:
|
|
"""
|
|
Initialize analyzer with English letter frequency reference data
|
|
"""
|
|
self.reference_frequencies = ENGLISH_LETTER_FREQUENCIES
|
|
|
|
def calculate_chi_squared(self, text: str) -> float:
|
|
"""
|
|
Calculate chi-squared statistic comparing text to expected English frequencies
|
|
"""
|
|
text_upper = text.upper()
|
|
letter_counts = Counter(char for char in text_upper if char.isalpha())
|
|
|
|
if not letter_counts:
|
|
return float("inf")
|
|
|
|
total_letters = sum(letter_counts.values())
|
|
chi_squared = 0.0
|
|
|
|
for letter, expected_freq in self.reference_frequencies.items():
|
|
observed_count = letter_counts.get(letter, 0)
|
|
expected_count = (expected_freq / 100) * total_letters
|
|
|
|
if expected_count > 0:
|
|
chi_squared += (
|
|
(observed_count - expected_count)**2
|
|
) / expected_count
|
|
|
|
return chi_squared
|
|
|
|
def score_text(self, text: str) -> float:
|
|
"""
|
|
Score text likelihood of being valid English (lower is better)
|
|
"""
|
|
return self.calculate_chi_squared(text)
|
|
|
|
def rank_candidates(self,
|
|
candidates: list[tuple[int,
|
|
str]]) -> list[tuple[int,
|
|
str,
|
|
float]]:
|
|
"""
|
|
Rank decryption candidates by their English frequency score
|
|
"""
|
|
scored = [
|
|
(shift,
|
|
text,
|
|
self.score_text(text)) for shift, text in candidates
|
|
]
|
|
return sorted(scored, key = lambda x: x[2])
|