<commit_before>from enum import Enum
from functools import reduce
from itertools import groupby
from io import StringIO
from math import gcd
from typing import Iterable, Iterator, List, NamedTuple, Type

from ..timing import Beat
from ..types import Chart


__all__ = ['NoteType', 'Note', 'NoteData']


class NoteType(Enum):
    """
    Known note types supported by StepMania.
    """
    TAP = '1'
    HOLD_HEAD = '2'
    TAIL = '3'
    ROLL_HEAD = '4'
    ATTACK = 'A'
    FAKE = 'F'
    KEYSOUND = 'K'
    LIFT = 'L'
    MINE = 'M'
    

class Note(NamedTuple):
    """
    A note, corresponding to a nonzero character in a chart's note data.
    """
    beat: Beat
    column: int
    note_type: NoteType


class NoteData:
    """
    Wrapper for note data with iteration & serialization capabilities.
    """
    _notedata: str
    _columns: int

    @property
    def columns(self):
        """How many note columns this chart has."""
        return self._columns

    def __init__(self, notedata: str):
        self._notedata = notedata
        self._columns = NoteData._get_columns(notedata)
    
    @staticmethod
    def _get_columns(notes: str):
        first_comma = notes.find(',')
        first_measure = notes[:first_comma] if first_comma > 0 else notes
        first_line = first_measure.strip().splitlines()[0].strip()
        return len(first_line)
    
    @classmethod
    def from_notes(
        cls: Type['NoteData'],
        notes: Iterator[Note],
        columns: int
    ) -> 'NoteData':
        """
        Convert a stream of notes into note data.

        This method assumes the following preconditions:

        * The input notes are sorted chronologically (ascending beats).
        * Every note's beat is nonnegative.
        * Every note's column is nonnegative and less than `columns`.

        Note that this method doesn't quantize beats to 192nd ticks,
        and off-grid notes may result in measures with more rows than
        the StepMania editor would produce. StepMania will quantize
        these notes gracefully during gameplay, but you can apply
        :meth:`simfile.timing.Beat.round_to_tick` to each note's beat
        if you'd prefer to keep the note data tidy.
        """
        notedata = StringIO()

        # write a row and trailing newline to the notedata
        def push_row(row: List[Note] = []):
            chars = ['0'] * columns
            for note in row:
                chars[note.column] = note.note_type.value
            notedata.write(''.join(chars))
            notedata.write('\n')

        # write a measure to the notedata (no commas or newlines of its own)
        def push_measure(measure: List[Note] = []):
            # get all beat quantizations from this measure
            quantizations = map(lambda note: note.beat.denominator, measure)
            # find the least common multiple of these quantizations
            q = reduce(lambda a, b: a * b // gcd(a, b), quantizations, 1)
            
            # group notes by row
            # the expression `note.beat % 4 * q` should always resolve to an
            # integer because `q` is a multiple of every beat's denominator
            last_row = -1
            for r, row in groupby(measure, lambda note: int(note.beat % 4 * q)):
                # account for any skipped beats
                for _ in range(last_row+1, r):
                    push_row()
                push_row(list(row))
                last_row = r
            # account for any trailing empty rows
            for _ in range(last_row+1, q*4):
                push_row()
        
        # group notes by measure
        last_measure = -1
        for m, measure in groupby(notes, lambda n: n.beat // 4):
            # handling the comma at the start of the loop instead of the end
            # avoids needing to know when we've reached the last measure
            if last_measure > -1:
                notedata.write(',\n')
            # account for any skipped measures
            for _ in range(last_measure+1, m):
                push_measure()
                notedata.write(',\n')
            push_measure(list(measure))
            last_measure = m
        
        # if there were no notes at all, write a blank measure
        if last_measure == -1:
            push_measure()
        
        return cls(notedata.getvalue())

            
    
    @classmethod
    def from_chart(cls: Type['NoteData'], chart: Chart) -> 'NoteData':
        """
        Get note data from a chart.
        """
        if 'NOTES2' in chart:<commit_msg>make notes comparable<commit_after>from enum import Enum
from functools import reduce, total_ordering
from itertools import groupby
from io import StringIO
from math import gcd
from typing import Iterator, List, NamedTuple, Type

from ..timing import Beat
from ..types import Chart


__all__ = ['NoteType', 'Note', 'NoteData']


class NoteType(Enum):
    """
    Known note types supported by StepMania.
    """
    TAP = '1'
    HOLD_HEAD = '2'
    TAIL = '3'
    ROLL_HEAD = '4'
    ATTACK = 'A'
    FAKE = 'F'
    KEYSOUND = 'K'
    LIFT = 'L'
    MINE = 'M'
    

@total_ordering
class Note(NamedTuple):
    """
    A note, corresponding to a nonzero character in a chart's note data.
    """
    beat: Beat
    column: int
    note_type: NoteType

    def __lt__(self, other) -> bool:
        """
        Compare to another note first by beat, then by column.
        """
        if self.beat < other.beat:
            return True
        if self.beat == other.beat:
            if self.column < other.column:
                return True
        return False


class NoteData:
    """
    Wrapper for note data with iteration & serialization capabilities.
    """
    _notedata: str
    _columns: int

    @property
    def columns(self):
        """How many note columns this chart has."""
        return self._columns

    def __init__(self, notedata: str):
        self._notedata = notedata
        self._columns = NoteData._get_columns(notedata)
    
    @staticmethod
    def _get_columns(notes: str):
        first_comma = notes.find(',')
        first_measure = notes[:first_comma] if first_comma > 0 else notes
        first_line = first_measure.strip().splitlines()[0].strip()
        return len(first_line)
    
    @classmethod
    def from_notes(
        cls: Type['NoteData'],
        notes: Iterator[Note],
        columns: int
    ) -> 'NoteData':
        """
        Convert a stream of notes into note data.

        This method assumes the following preconditions:

        * The input notes are sorted chronologically (ascending beats).
        * Every note's beat is nonnegative.
        * Every note's column is nonnegative and less than `columns`.

        Note that this method doesn't quantize beats to 192nd ticks,
        and off-grid notes may result in measures with more rows than
        the StepMania editor would produce. StepMania will quantize
        these notes gracefully during gameplay, but you can apply
        :meth:`simfile.timing.Beat.round_to_tick` to each note's beat
        if you'd prefer to keep the note data tidy.
        """
        notedata = StringIO()

        # write a row and trailing newline to the notedata
        def push_row(row: List[Note] = []):
            chars = ['0'] * columns
            for note in row:
                chars[note.column] = note.note_type.value
            notedata.write(''.join(chars))
            notedata.write('\n')

        # write a measure to the notedata (no commas or newlines of its own)
        def push_measure(measure: List[Note] = []):
            # get all beat quantizations from this measure
            quantizations = map(lambda note: note.beat.denominator, measure)
            # find the least common multiple of these quantizations
            q = reduce(lambda a, b: a * b // gcd(a, b), quantizations, 1)
            
            # group notes by row
            # the expression `note.beat % 4 * q` should always resolve to an
            # integer because `q` is a multiple of every beat's denominator
            last_row = -1
            for r, row in groupby(measure, lambda note: int(note.beat % 4 * q)):
                # account for any skipped beats
                for _ in range(last_row+1, r):
                    push_row()
                push_row(list(row))
                last_row = r
            # account for any trailing empty rows
            for _ in range(last_row+1, q*4):
                push_row()
        
        # group notes by measure
        last_measure = -1
        for m, measure in groupby(notes, lambda n: n.beat // 4):
            # handling the comma at the start of the loop instead of the end
            # avoids needing to know when we've reached the last measure
            if last_measure > -1:
                notedata.write(',\n')
            # account for any skipped measures
            for _ in range(last_measure+1, m):
                push_measure()
                notedata.write(',\n')
            push_measure(list(measure))
            last_measure = m
        
        # if there were no notes at all, write a blank measure
        if last_measure == -1:
            push_measure()
        
        return cls(notedata.getvalue())

    @classmethod
    def from_chart(cls: Type['NoteData'], chart: Chart) -> 'NoteData':
        """
        Get note data from a chart.
        """
        if 'NOTES2' in chart:
{'fraction_non_alphanumeric': 0.08698412698412698, 'fraction_numerical': 0.004867724867724868, 'mean_word_length': 1.9241955445544554, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 12, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '484749', 'n_tokens_mistral': 2688, 'n_tokens_neox': 2574, 'n_words': 943}