| 1 | 1 | class Scale: |
| 2 | 2 | def __init__(self, tonic: str): |
| 3 | | pass |
| 3 | # Edge Case: Normalize tonic to handle case insensitivity |
| 4 | self.tonic = tonic.capitalize() if len(tonic) == 1 else tonic[0].capitalize() + tonic[1:] |
| 5 | |
| 6 | # Define the chromatic scales with sharps and flats |
| 7 | self.sharps_scale = ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"] |
| 8 | self.flats_scale = ["A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab"] |
| 9 | |
| 10 | # Determine whether to use sharps or flats based on the tonic |
| 11 | # Edge Case: Handle special cases for C/a which follow sharp convention when ascending |
| 12 | sharps_keys = {"G", "D", "A", "E", "B", "F#", "C"} |
| 13 | flats_keys = {"F", "Bb", "Eb", "Ab", "Db", "Gb"} |
| 14 | |
| 15 | minor_sharps = {"e", "b", "f#", "c#", "g#", "d#", "c"} |
| 16 | minor_flats = {"d", "g", "c", "f", "bb", "eb"} |
| 17 | |
| 18 | # Edge Case: Handle minor tonics with lowercase input |
| 19 | if tonic.islower(): |
| 20 | if tonic in minor_sharps: |
| 21 | self.use_flats = False |
| 22 | elif tonic in minor_flats: |
| 23 | self.use_flats = True |
| 24 | else: # Natural minor "a" |
| 25 | self.use_flats = False |
| 26 | # For minor keys, we need to normalize the tonic for scale lookup |
| 27 | # but keep the original case for key determination |
| 28 | if len(tonic) == 1: |
| 29 | normalized_tonic = tonic.upper() |
| 30 | else: |
| 31 | normalized_tonic = tonic[0].upper() + tonic[1:] |
| 32 | self.tonic = normalized_tonic |
| 33 | else: # Major tonics (uppercase) |
| 34 | if self.tonic in sharps_keys: |
| 35 | self.use_flats = False |
| 36 | elif self.tonic in flats_keys: |
| 37 | self.use_flats = True |
| 38 | else: # Natural major "C" |
| 39 | self.use_flats = False |
| 40 | |
| 41 | # Edge Case: Determine the scale and starting index based on the tonic |
| 42 | if self.use_flats: |
| 43 | self.scale = self.flats_scale |
| 44 | else: |
| 45 | self.scale = self.sharps_scale |
| 46 | |
| 47 | # Find the starting index in the scale |
| 48 | # Edge Case: Handle potential missing tonic in the selected scale |
| 49 | try: |
| 50 | self.start_index = self.scale.index(self.tonic) |
| 51 | except ValueError: |
| 52 | # This should not happen with correct logic, but as a safeguard |
| 53 | # Edge Case: Fallback to sharps scale if tonic not found in flats |
| 54 | self.scale = self.sharps_scale |
| 55 | self.start_index = self.scale.index(self.tonic) |
| 4 | 56 | |
| 5 | 57 | def chromatic(self) -> list[str]: |
| 6 | | pass |
| 58 | # Edge Case: Return all 12 notes starting from the tonic |
| 59 | result = [] |
| 60 | for i in range(12): |
| 61 | index = (self.start_index + i) % 12 |
| 62 | result.append(self.scale[index]) |
| 63 | return result |
| 7 | 64 | |
| 8 | 65 | def interval(self, intervals: str) -> list[str]: |
| 9 | | pass |
| 66 | # Edge Case: Handle empty interval string |
| 67 | if not intervals: |
| 68 | return [self.tonic] |
| 69 | |
| 70 | # Define interval mappings |
| 71 | # Edge Case: Validate interval characters |
| 72 | interval_steps = { |
| 73 | "m": 1, # minor second (half step) |
| 74 | "M": 2, # major second (whole step) |
| 75 | "A": 3 # augmented second (three semitones) |
| 76 | } |
| 77 | |
| 78 | result = [self.tonic] |
| 79 | current_index = self.start_index |
| 80 | |
| 81 | # Edge Case: Process each interval in the pattern |
| 82 | for interval_char in intervals: |
| 83 | # Edge Case: Handle invalid interval characters |
| 84 | if interval_char not in interval_steps: |
| 85 | raise ValueError(f"Invalid interval character: {interval_char}") |
| 86 | |
| 87 | step = interval_steps[interval_char] |
| 88 | current_index = (current_index + step) % 12 |
| 89 | result.append(self.scale[current_index]) |
| 90 | |
| 91 | return result |
| 92 | # Handled Edge Cases: Normalize tonic case, determine sharp/flat usage, handle special cases for C/a, handle minor tonics, find tonic in scale, return all 12 notes, handle empty intervals, validate interval characters, process interval pattern, handle invalid intervals |