<commit_before>"""
Module for creating valid OPM files as specified in the Orbit Data Message
Recommended Standard CCSDS 502.0-B-2

Recommended import syntax:
import orbitdatamessages.orbital_parameter_message as opm
"""
import re
import textwrap
from datetime import datetime
from enum import Enum
from math import floor, log10
from numbers import Number

# from orbitdatamessages.orbital_parameter_message import * considered harmful
# Even so, make sure only core functionality gets imported
__all__ = [
    'DataBlockCovarianceMatrix',
    'DataBlockKeplerianElements',
    'DataBlockManeuverParameters',
    'DataBlockSpacecraftParameters',
    'DataBlockStateVector',
    'Header',
    'Metadata',
    'Opm',
    'RefFrame',
    'TimeSystem',
]


def prefix(prefix, iterable):
    """Add prefix to each item of an iterable."""
    for x in iterable:
        yield '{prefix}{x!s}'.format(prefix=prefix, x=x)


def suffix(suffix, iterable):
    """Add suffix to each item of an iterable."""
    for x in iterable:
        yield '{x!s}{suffix!s}'.format(suffix=suffix, x=x)


def _mant_exp(num):
    """
    Return the mantissa and (base 10) exponent of num.

    num -- float or int.
    """
    try:
        exponent = floor(log10(abs(num)))
    except ValueError:  # Case of log10(0)
        return (0, 0)  # Convention: 0 = 0*10^0
    mantissa = num / 10**exponent
    return (mantissa, int(exponent))


def format_number(number):
    """Format number for ODM output.

    Number is truncated to MAX_DIGITS if required. Numbers are given leading
    space such that any number passed in will have its decimal point in the
    same character position.
    """
    MAX_DIGITS = 16  # From ODM spec

    _, exponent = _mant_exp(number)

    if exponent > MAX_DIGITS - 2:
        return ' ' * (MAX_DIGITS - 1) + '{: .15e}'.format(number)
    elif exponent < -4:
        return ' ' * (MAX_DIGITS - 1) + '{: .15e}'.format(number)
    else:
        if exponent > 0:
            prec = '{: {padding}.{precision}f}'.format(
                number, precision=MAX_DIGITS - 1 - exponent,
                padding=MAX_DIGITS * 2)
            ind = exponent - 1
            prec = prec[ind:]
        elif exponent <= 0:
            prec = '{: {padding}.{precision}f}'.format(
                number, precision=MAX_DIGITS - 2, padding=MAX_DIGITS * 2)
        return prec


def validate_string(string):
    """Check string is non-empty."""
    return True if string else False


def validate_date(date):
    return date.utcoffset() is None


def validate_object_id(object_id):
    match = re.match('^[0-9]{4}\-[0-9]{3}[A-Z]{1,3}$', object_id)
    return True if match else False


class TimeSystem(Enum):
    GMST = 'GMST'
    GPS  = 'GPS'
    MET  = 'MET'
    MRT  = 'MRT'
    SCLK = 'SCLK'
    TAI  = 'TAI'
    TCB  = 'TCB'
    TDB  = 'TDB'
    TCG  = 'TCG'
    TT   = 'TT'
    UT1  = 'UT1'
    UTC  = 'UTC'


class RefFrame(Enum):
    EME2000  = 'EME2000'
    GCRF     = 'GCRF'
    GRC      = 'GRC'
    ICRF     = 'ICRF'
    ITRF2000 = 'ITRF2000'
    ITRF_93  = 'ITRF_93'
    ITRF_97  = 'ITRF_97'
    MCI      = 'MCI'
    TDR      = 'TDR'
    TEME     = 'TEME'
    TOD      = 'TOD'
    RSW      = 'RSW'
    RTN      = 'RTN'
    TNW      = 'TNW'


class MissingKeywordError(Exception):
    pass


class DuplicateKeywordError(Exception):
    pass


class Keyword:

    """ODM keyword object.

    Apart from the keyword name and value, the 'mandatory',
    'formatter', and 'validator' properties ensure that only a valid Keyword
    can be written to file, formatted appropriately.
    """

    def __init__(self, keyword, value=None, mandatory=True,
                 formatter=lambda x: x, validator=lambda x: True):
        """Initialise keyword with sane defaults.

        Note that keywords are mandatory by default, i.e. optional keywords
        are opt-out.

        formatter and validator are instance variables so that this information
        is associated directly with the keyword. This also allows individual
        keywords to have either method overriden for special cases.
        """
        self.keyword = keyword
        self.mandatory = mandatory
        self.value = value
        self.formatter = formatter
        self.validator = validator

    @property
    def formatted_value(self):
        """Format keyword value for writing to file."""
        return self.formatter(self.value)

    def is_valid(self):
        """Check if keyword is valid."""
        return self.validator(self.value)


class DataKeyword(Keyword):

    """Subclass of Keyword for keywords with units."""

    def __init__(self, keyword, value=None, units=None, mandatory=True,
                 formatter=lambda x: x, validator=lambda x: True):
        """Initialise super & set instance variables unique to DataKeyword."""
        super().__init__(keyword=keyword, value=value, mandatory=mandatory,
                         formatter=formatter, validator=validator)
        self.units = units


class KeywordContainer:

    """Base class of OPM keyword sections.

    The standard splits an orbital parameter message file into different
    sections, which will subclass KeywordContainer.

    The main purpose of the base class is to implement the methods for
    validating and formatting the list of keywords provided by each sublcass.
    """
<commit_msg>Add __repr__ to keyword classes<commit_after>"""
Module for creating valid OPM files as specified in the Orbit Data Message
Recommended Standard CCSDS 502.0-B-2

Recommended import syntax:
import orbitdatamessages.orbital_parameter_message as opm
"""
import re
import textwrap
from datetime import datetime
from enum import Enum
from math import floor, log10
from numbers import Number

# from orbitdatamessages.orbital_parameter_message import * considered harmful
# Even so, make sure only core functionality gets imported
__all__ = [
    'DataBlockCovarianceMatrix',
    'DataBlockKeplerianElements',
    'DataBlockManeuverParameters',
    'DataBlockSpacecraftParameters',
    'DataBlockStateVector',
    'Header',
    'Metadata',
    'Opm',
    'RefFrame',
    'TimeSystem',
]


def prefix(prefix, iterable):
    """Add prefix to each item of an iterable."""
    for x in iterable:
        yield '{prefix}{x!s}'.format(prefix=prefix, x=x)


def suffix(suffix, iterable):
    """Add suffix to each item of an iterable."""
    for x in iterable:
        yield '{x!s}{suffix!s}'.format(suffix=suffix, x=x)


def _mant_exp(num):
    """
    Return the mantissa and (base 10) exponent of num.

    num -- float or int.
    """
    try:
        exponent = floor(log10(abs(num)))
    except ValueError:  # Case of log10(0)
        return (0, 0)  # Convention: 0 = 0*10^0
    mantissa = num / 10**exponent
    return (mantissa, int(exponent))


def format_number(number):
    """Format number for ODM output.

    Number is truncated to MAX_DIGITS if required. Numbers are given leading
    space such that any number passed in will have its decimal point in the
    same character position.
    """
    MAX_DIGITS = 16  # From ODM spec

    _, exponent = _mant_exp(number)

    if exponent > MAX_DIGITS - 2:
        return ' ' * (MAX_DIGITS - 1) + '{: .15e}'.format(number)
    elif exponent < -4:
        return ' ' * (MAX_DIGITS - 1) + '{: .15e}'.format(number)
    else:
        if exponent > 0:
            prec = '{: {padding}.{precision}f}'.format(
                number, precision=MAX_DIGITS - 1 - exponent,
                padding=MAX_DIGITS * 2)
            ind = exponent - 1
            prec = prec[ind:]
        elif exponent <= 0:
            prec = '{: {padding}.{precision}f}'.format(
                number, precision=MAX_DIGITS - 2, padding=MAX_DIGITS * 2)
        return prec


def validate_string(string):
    """Check string is non-empty."""
    return True if string else False


def validate_date(date):
    return date.utcoffset() is None


def validate_object_id(object_id):
    match = re.match('^[0-9]{4}\-[0-9]{3}[A-Z]{1,3}$', object_id)
    return True if match else False


class TimeSystem(Enum):
    GMST = 'GMST'
    GPS  = 'GPS'
    MET  = 'MET'
    MRT  = 'MRT'
    SCLK = 'SCLK'
    TAI  = 'TAI'
    TCB  = 'TCB'
    TDB  = 'TDB'
    TCG  = 'TCG'
    TT   = 'TT'
    UT1  = 'UT1'
    UTC  = 'UTC'


class RefFrame(Enum):
    EME2000  = 'EME2000'
    GCRF     = 'GCRF'
    GRC      = 'GRC'
    ICRF     = 'ICRF'
    ITRF2000 = 'ITRF2000'
    ITRF_93  = 'ITRF_93'
    ITRF_97  = 'ITRF_97'
    MCI      = 'MCI'
    TDR      = 'TDR'
    TEME     = 'TEME'
    TOD      = 'TOD'
    RSW      = 'RSW'
    RTN      = 'RTN'
    TNW      = 'TNW'


class MissingKeywordError(Exception):
    pass


class DuplicateKeywordError(Exception):
    pass


class Keyword:

    """ODM keyword object.

    Apart from the keyword name and value, the 'mandatory',
    'formatter', and 'validator' properties ensure that only a valid Keyword
    can be written to file, formatted appropriately.
    """

    def __init__(self, keyword, value=None, mandatory=True,
                 formatter=lambda x: x, validator=lambda x: True):
        """Initialise keyword with sane defaults.

        Note that keywords are mandatory by default, i.e. optional keywords
        are opt-out.

        formatter and validator are instance variables so that this information
        is associated directly with the keyword. This also allows individual
        keywords to have either method overriden for special cases.
        """
        self.keyword = keyword
        self.mandatory = mandatory
        self.value = value
        self.formatter = formatter
        self.validator = validator

    def __repr__(self):
        return ('{name}('
                'keyword={keyword!r}, '
                'value={value!r}, '
                'mandatory={mandatory!r})'
               ).format(
                    name=self.__class__.__name__,
                    keyword=self.keyword,
                    value=self.value,
                    mandatory=self.mandatory)

    @property
    def formatted_value(self):
        """Format keyword value for writing to file."""
        return self.formatter(self.value)

    def is_valid(self):
        """Check if keyword is valid."""
        return self.validator(self.value)


class DataKeyword(Keyword):

    """Subclass of Keyword for keywords with units."""

    def __init__(self, keyword, value=None, units=None, mandatory=True,
                 formatter=lambda x: x, validator=lambda x: True):
        """Initialise super & set instance variables unique to DataKeyword."""
        super().__init__(keyword=keyword, value=value, mandatory=mandatory,
                         formatter=formatter, validator=validator)
        self.units = units

    def __repr__(self):
        return ('{name}('
                'keyword={keyword!r}, '
                'value={value!r}, '
                'mandatory={mandatory!r}, '
                'units={units!r})'
               ).format(
                    name=self.__class__.__name__,
                    keyword=self.keyword,
                    value=self.value,
                    mandatory=self.mandatory,
                    units=self.units)


class KeywordContainer:

    """Base class of OPM keyword sections.

    The standard splits an orbital parameter message file into different
    sections, which will subclass KeywordContainer.

    The main purpose of the base class is to implement the methods for
    validating and formatting the list of keywords provided by each sublcass.
    """

{'fraction_non_alphanumeric': 0.10261743803085457, 'fraction_numerical': 0.012827179753856822, 'mean_word_length': 2.4088626292466766, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 7, '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': '6880089', 'n_tokens_mistral': 3640, 'n_tokens_neox': 3387, 'n_words': 1072}