Skip to main content
`Ctrl`+`K`

# Source code for astropy.io.ascii.tdat

    # Licensed under a 3-clause BSD style license - see LICENSE.rst
    """This package contains functions for reading and writing TDAT tables that are
    not meant to be used directly, but instead are available as readers/writers in
    `astropy.table`. See :ref:`astropy:table_io` for more details.

    :Author: Daniel Giles (daniel.k.giles@gmail.com)
             Abdu Zoghbi

    """

    import re
    from copy import deepcopy
    from warnings import warn

    import numpy as np

    from astropy.utils.exceptions import AstropyWarning

    from . import basic, core

    _STD_MSG = "See details in https://heasarc.gsfc.nasa.gov/docs/software/dbdocs/tdat.html"

    def make_example_data():
        return [
            "<HEADER>",
            "# # and // are comments",
            "table_name = example_table",
            'table_description = "Example table"',
            "#",
            "# Table Parameters",
            "#",
            "field[id] = integer [meta.id] (key) // Unique ID",
            "field[ra] = float:.4f_degree [pos.eq.ra] (index) // Right Ascension",
            "field[name] = char12 [meta.id] // Name",
            "#",
            "# Virtual Parameters",
            "#",
            "table_author = Example et al.",
            "#",
            "# Data Format Specification",
            "#",
            "line[1] = id name ra",
            "<DATA>",
            "1|TargetOne|1.0|",
            "2|TargetTwo|2.0|",
            "<END>",
        ]

    class TdatFormatError(Exception):
        """Tdat Format Error"""

        def __str__(self):
            return f"{super().__str__()}\n{_STD_MSG}"

    class TdatFormatWarning(AstropyWarning):
        """Tdat Format Warning"""

    class TdatDataSplitter(core.BaseSplitter):
        """Splitter for tdat data.

        Handles the (deprecated) cases of multiple data delimiters, record
        delimiters, and multi-line records.
        Multiple data delimiters - Multiple delimiters can be specified in the
            header, e.g. field_delimiter = "|!" would treat both | and !
            as individual delimiters. Default: "|"
        Record Delimiters - The record_delimiter can be specified in the header. By
            default there is no record delimiter and new records should be set on
            new lines. The following list standard escaped character sequences and their
            equivalent meanings can be used:
            * \t (tab)
            * \b (backspace)
            * \r (carriage return)
            * \f (form feed)
            * \v (vertical tab)
            * \a (audible alert/bell),
            * \\### (where ### is a number between 1 and 127 and represents the
            ASCII character with that numerical code).
            Note: Specifying a record delimiter value of "" is interpreted as a
                single blank line between records.
        Multi-line records - A single record may take more than one line, indicated in the header

        """

        delimiter = "|"
        record_delimiter = None

        @property
        def literals_dict(self):
            """Return a dictionary of placeholders to be used in place of
            backslashed delimiter characters.
            """
            return {c: f"literal{n}" for n, c in enumerate(self.delimiter)}

        def preprocess_data_lines(self, lines, record_delimiter):
            """Split lines into multiple lines if a record_delimiter is specified."""
            data_lines = []
            for line in lines:
                data_lines += re.split(rf"{record_delimiter}", line)
            return data_lines

        def process_line(self, line: str) -> str:
            """Remove whitespace at the beginning or end of line.  This is especially useful for
            whitespace-delimited files to prevent spurious columns at the beginning or end.

            READ: override default to handle backslashed delimiter characters.
            """
            for c in self.delimiter:
                line = line.replace(f"\\{c}", self.literals_dict[c])
            line = re.sub(rf"[{self.delimiter}]", "|", line)
            return line.strip()

        def __call__(self, lines, field_delimiter="|", record_delimiter=None, nlines=1):
            """ """
            self.delimiter = field_delimiter

            def replace_placeholders(line):
                for c in field_delimiter:
                    line = line.replace(self.literals_dict[c], c)
                return line

            if " " in field_delimiter:
                warn(
                    TdatFormatWarning("Double check your data when using space delimiters.")
                )

            if record_delimiter is not None:
                lines = self.preprocess_data_lines(lines, record_delimiter)
            if hasattr(self, "process_line"):
                lines = (self.process_line(x) for x in lines)

            iline = 0
            _lines = []
            for line in lines:
                _lines.append(line)
                iline += 1
                if iline == nlines:
                    vals = [
                        replace_placeholders(val)
                        for _line in _lines
                        for val in re.split(r"\|", _line)[:-1]
                    ]
                    # Reset
                    iline = 0
                    _lines = []
                    if hasattr(self, "process_val"):
                        yield [self.process_val(x) for x in vals]
                    else:  # pragma: no cover
                        yield vals
                else:  # pragma: no cover
                    continue

        def join(self, vals):
            delimiter = getattr(self, "delimiter", "|")
            # TDAT specification requires a delimiter at the end of each record
            return delimiter.join(str(x) for x in vals) + delimiter

    class TdatHeader(basic.BasicHeader):
        """TDAT table header"""

        splitter_class = TdatDataSplitter
        cols = None
        comment = r"\s*(#|//)"
        # comments: # or //
        _comment = r"\s*(#|//)(.*)$"
        # descriptors that shouldn't be registered as comments
        _desc_comments = [
            "Table Parameters",
            "Virtual Parameters",
            "Relationship Definitions",
            "Data Format Specification",
        ]
        # keywords in the header: name = value
        _keys = r"(?P<key>\w+)\s*=\s*(?P<value>.*?)(\s*(#|//)(?P<comment>.*$)|(\s*$))"
        # keywords in the header: name[text] = some_other_text;
        # names: relate|line
        _extra_keys = r"\s*(relate|line)\[(\w+)\]\s*=\s*([\w\s]+)(?:\((\w+)\))?"
        _field_line = r"\s*field\[\w+\]\s*=\.*"

        _deprecated_keys = r"\s*(record_delimiter|field_delimiter)\s*=\s\"(.+)\""
        _required_keywords = ("table_name",)
        _dtype_dict_in = {
            "int1": np.int8,
            "integer1": np.int8,
            "tinyint": np.int8,
            "int2": np.int16,
            "integer2": np.int16,
            "smallint": np.int16,
            "int4": np.int32,
            "integer4": np.int32,
            "integer": np.int32,
            "float4": np.float32,
            "real": np.float32,
            "float": float,
            "float8": float,
        }

        _dtype_dict_out = {
            "int": "int4",
            "int32": "int4",
            "int16": "int2",
            "int8": "int1",
            "float": "float8",
            "float64": "float8",
            "float32": "float4",
        }

        def _validate_comment(self, line) -> bool:
            """Check if line is a valid comment, return comment if so"""
            comment_parser = re.compile(self._comment)
            cmatch = comment_parser.match(line)
            if cmatch:
                # Ignore common section headers
                for cc in self._desc_comments:
                    if cc in cmatch.group(2):
                        return False
                # Ignore empty comments
                return cmatch.group(2) != ""
            return False

        def _process_comment(self, line: str) -> str:
            return re.sub("^" + self.comment, "", line).strip()

        def _process_keywords(self, lines):
            line_fields = {}
            keywords = getattr(self, "_keywords", {})
            keys_parser = re.compile(self._keys)
            extra_keys_parser = re.compile(self._extra_keys)
            deprecated_keys_parser = re.compile(self._deprecated_keys)

            for line in lines:
                kmatch = keys_parser.match(line)
                ematch = extra_keys_parser.match(line)
                dmatch = deprecated_keys_parser.match(line)
                if dmatch:
                    warn(
                        f'"{dmatch.group(1)}" keyword is deprecated. {_STD_MSG}',
                        TdatFormatWarning,
                    )
                    if dmatch.group(1) == "record_delimiter":
                        self.record_delimiter = dmatch.group(2)
                    elif dmatch.group(1) == "field_delimiter":
                        self.delimiter = dmatch.group(2)
                elif ematch:
                    # match extra keywords
                    if ematch.group(1) == "relate":
                        warn(
                            f'"relate" keyword is obsolete and will be ignored. {_STD_MSG}',
                            TdatFormatWarning,
                        )
                    elif ematch.group(1) == "line":
                        # fields in line; typically only 1, but can be more
                        line_fields[ematch.group(2)] = ematch.group(3).split()

                elif kmatch:
                    key = kmatch.group("key")
                    _quotes = r"^(?P<quote>[\"\'\`]*)(?P<value>.*?)(?P<endquote>[\"\'\`]*$)"
                    quote_parse = re.compile(_quotes)
                    quotes_match = quote_parse.match(kmatch.group("value"))
                    if quotes_match.group("quote") != quotes_match.group("endquote")[::-1]:
                        raise TdatFormatError("Mismatched quotes for value of " + key)

                    value = kmatch.group("value").strip("\"'`")
                    if key == "table_name":
                        if len(value) > 20:
                            warn(
                                "The table_name has a maximum length of 20 characters, truncating.",
                                TdatFormatWarning,
                            )
                            value = value[:20]
                    elif key == "table_description":
                        if len(value) > 80:
                            warn(
                                "The table_description has a maximum length of 80 characters, truncating.",
                                TdatFormatWarning,
                            )
                            value = value[:80]
                    elif key == "table_security":
                        if value.lower() not in ("public", "private"):
                            warn(
                                "Value for table_security not recognized, should be public|private.",
                                TdatFormatWarning,
                            )
                    keywords[key] = value

            self._line_fields = line_fields

            if len(keywords) > len(getattr(self, "_keywords", {})):
                self._keywords = keywords

        def update_meta(self, lines, meta):
            """Extract meta information: comments and key/values in the header
            READ: Overrides the default update_meta
            """
            try:
                start_line = (
                    min(
                        i
                        for i, line in enumerate(lines)
                        if line.strip().upper() == "<HEADER>"
                    )
                    + 1
                )
            except ValueError:
                raise TdatFormatError("<HEADER> not found in file." + _STD_MSG)
            try:
                end_line = min(
                    i for i, line in enumerate(lines) if line.strip().upper() == "<DATA>"
                )
            except ValueError:
                raise TdatFormatError("<DATA> not found in file." + _STD_MSG)

            meta["table"]["comments"] = [
                self._process_comment(line)
                for line in lines[start_line:end_line]
                if self._validate_comment(line)
            ]
            self._process_keywords(lines)
            meta["table"]["keywords"] = self._keywords

        def process_lines(self, lines):
            """Select and process lines between <HEADER> and <DATA> lines
            READ: Override default process_lines
            """
            fl_parser = re.compile(self._field_line)
            start_line = (
                min(i for i, line in enumerate(lines) if line.strip().upper() == "<HEADER>")
                + 1
            )
            end_line = min(
                i for i, line in enumerate(lines) if line.strip().upper() == "<DATA>"
            )

            for line in lines[start_line:end_line]:
                if fl_parser.match(line):
                    yield line

        def get_cols(self, lines):
            """Initialize the header Column objects from TDAT field lines
            READ: Overrides default get_cols
            """
            col_parser = re.compile(
                r"""
                \s*field
                \[(?P<name>\w+)\]\s*=\s*
                (?P<ctype>[^\W\s_]+(\(\d+\))?)
                (?:\:(?P<fmt>[^_\s\[\(/#]+))?
                (?:_(?P<unit>[^\[\(#]+))?\s*
                (?:\[(?P<ucd>[\w\.\;]+)\])?
                \s*
                (?:\((?P<index>\w+)\))?
                \s*
                (?:(//|\#)\s*(?P<desc>[^/#]*))?
                (?:(//|\#)\s*(?P<comment>.*))?
                \s*
                """,
                re.VERBOSE,
            )

            cols = {}
            keywords = getattr(self, "_keywords", {})
            for line in self.process_lines(lines):
                # look for field[..]= ... column definitions
                cmatch = col_parser.match(line)
                if cmatch:
                    name = cmatch.group("name")
                    if len(name) > 24:
                        warn(
                            "The field name must be shorter than 24 characters.",
                            TdatFormatWarning,
                        )
                    col = core.Column(name=name)
                    col.meta = {}

                    ctype = cmatch.group("ctype")
                    if ctype in self._dtype_dict_in:
                        col.dtype = self._dtype_dict_in[ctype]
                    elif "char" in ctype:
                        col.dtype = str
                    else:
                        raise TdatFormatError(
                            f"Unrecognized or unsupported data type {ctype} for {col.name}."
                        )
                    col.unit = cmatch.group("unit")
                    col.format = cmatch.group("fmt")
                    if len(str(ctype) + str(col.format)) > 23:
                        # max 24 characters with ":" separator
                        raise TdatFormatError(
                            "The type:fmt specifier has a\
                            maximum length of 24 characters. The offending line is:\
                                \n{line}\n"
                            + _STD_MSG
                        )
                    col.description = f"{cmatch.group('desc')}".strip()
                    for val in ["comment", "ucd", "index"]:
                        if cmatch.group(val) is not None:
                            text = cmatch.group(val).strip()
                            if (val == "comment") and (len(text) > 80):
                                warn(
                                    TdatFormatWarning(
                                        "Comments are limited to 80 characters or less, truncating."
                                    )
                                )
                            col.meta[val] = text[:80]
                    cols[col.name] = col

            self.names = [
                val for line_field in self._line_fields.values() for val in line_field
            ]

            # check that cols and _line_fields are consistent or throw an error
            if len(self.names) != len(cols):
                colnames = [col.name for col in cols.values()]
                raise TdatFormatError(
                    'The columns "field" descriptors are not consistent with '
                    "the line[..] keyword.\n"
                    f'"field" values: {colnames}\n'
                    f"line[..] values: {self.names}"
                )
            self.cols = []
            for name in self.names:
                self.cols.append(cols[name])

        def write_comments(self, lines, meta):
            """Write comment lines in the header
            WRITE: Override the default write_comments to include <HEADER> as first line
            """
            lines.append("<HEADER>")
            if self.write_comment not in (False, None):
                for comment in meta.get("comments", []):
                    lines.append(self.write_comment + comment.lstrip())

        def write(self, lines):
            """Write the Table out to a TDAT formatted file."""
            # the reader can handle alternate delimiters, but the writer should
            # comply with the current convention
            if self.splitter.delimiter.strip() != "|":
                warn(
                    TdatFormatWarning(
                        "Delimiters other than the pipe character, '|', are deprecated. Using '|'."
                    )
                )
            self.splitter.delimiter = "|"

            # Write the keywords and column descriptors
            keywords = deepcopy(self.table_meta.get("keywords", {}))
            meta_keys = ["keywords", "comments"]
            # In case a user puts a keyword directly in meta, instead of meta.keywords
            for key in [
                key.lower()
                for key in self.table_meta.keys()
                if key.lower() not in meta_keys
            ]:
                keywords[key] = self.table_meta.get(key)

            indices = [col.info.name for col in self.cols if col.info.indices != []]

            if "table_name" in keywords:
                if len(keywords["table_name"]) > 20:
                    warn(
                        "'table_name' is too long, truncating to 20 characters",
                        TdatFormatWarning,
                    )
                lines.append(f"table_name = {keywords.pop('table_name')[:20]}")
            else:
                warn(
                    "'table_name' must be specified\n"  # noqa: ISC003
                    + f"{_STD_MSG}\n"
                    + "This should be specified in the Table.meta.\n"
                    + "default value of 'astropy_table' being assigned.",
                    TdatFormatWarning,
                )
                lines.append("table_name = astropy_table")

            # loop through optional table keywords
            table_keywords = [
                "table_description",
                "table_document_url",
                "table_security",
            ]
            table_keywords = [kw for kw in table_keywords if kw in keywords.keys()]
            for kw in ["record_delimiter", "field_delimiter"]:
                val = keywords.pop(kw, None)
                if val is not None:
                    warn(TdatFormatWarning(f"Skipping deprecated keyword: {kw}."))

            for key in table_keywords:
                if key == "table_description":
                    if len(keywords[key]) > 80:
                        warn(
                            "'table_description' is too long, truncating to 80 characters",
                            TdatFormatWarning,
                        )
                    new_desc = keywords.pop(key)
                    new_desc = new_desc[:80]
                    lines.append(f"{key} = {new_desc}")
                elif key == "table_security":
                    if keywords.get(key).lower() not in ("public", "private"):
                        warn(
                            "Value for table_security not recognized, should be public|private.",
                            TdatFormatWarning,
                        )
                    lines.append(f"{key} = {keywords.pop(key)}")
                elif key in keywords:
                    lines.append(f"{key} = {keywords.pop(key)}")

            # add table columns as fields
            lines.append("#")
            lines.append("# Table Parameters")
            lines.append("#")
            for col in self.cols:
                if str(col_type := col.info.dtype) in self._dtype_dict_out:
                    ctype = self._dtype_dict_out[str(col_type)]
                elif col_type.kind == "i":
                    ctype = "int4"
                elif col_type.kind == "f":
                    ctype = "float8"
                elif col_type.kind == "U":
                    ctype = f"char{col_type.itemsize // 4}"
                else:
                    raise TdatFormatError(
                        f'Unrecognized data type `{col_type}` for column "{col.info.name}".'
                    )
                col_name = col.info.name
                if len(col_name) >= 24:
                    warn(
                        "The field name must be shorter than 24 characters, truncating.",
                        TdatFormatWarning,
                    )
                    col_name = col_name[:23]
                field_line = f"field[{col_name}] = {ctype}"

                col_info_meta = col.info.meta or {}
                if col.info.format is not None:
                    field_line += f":{col.info.format}"
                if col.info.unit is not None:
                    field_line += f"_{col.info.unit:cds}"
                if "ucd" in col_info_meta:
                    field_line += f" [{col_info_meta['ucd']}]"
                if "index" in col_info_meta:
                    field_line += f" ({col_info_meta['index']})"
                elif (indices != []) and (col.info.name == indices[0]):
                    field_line += " (key)"
                elif col.info.name in indices:
                    field_line += " (index)"
                if col.info.description is not None and col.info.description != "None":
                    field_line += f" // {col.info.description}"
                elif "comment" in col_info_meta:
                    field_line += " //"
                if "comment" in col_info_meta:
                    field_line += f" // {col_info_meta['comment']}"
                lines.append(field_line)

            if len(keywords) != 0:
                if "parameter_defaults" in keywords:
                    lines.append("#")
                    lines.append(
                        f"{'parameter_defaults'} = {keywords.pop('parameter_defaults')}"
                    )
            if len(keywords) != 0:
                lines.append("#")
                lines.append("# Virtual Parameters")
                lines.append("#")
                for key in keywords:
                    lines.append(f"{key} = {keywords[key]}")
            lines.append("#")
            lines.append("# Data Format Specification")
            lines.append("#")
            lines.append(f"line[1] = {' '.join([col.info.name for col in self.cols])}")
            lines.append("#")

    class TdatData(core.BaseData):
        """Data Reader for TDAT format"""

        comment = r"\s*(#|//)"
        write_comment = "# "
        splitter_class = TdatDataSplitter

        def get_data_lines(self, lines):
            """
            READ: Override the default get_data_lines to find start and end lines.
            """
            # Select lines between <DATA> and <END> in file
            try:
                start_line = (
                    min(
                        i
                        for i, line in enumerate(lines)
                        if line.strip().upper() == "<DATA>"
                    )
                    + 1
                )
            except ValueError:
                raise TdatFormatError("<DATA> not found in file." + _STD_MSG)
            try:
                end_line = min(
                    i for i, line in enumerate(lines) if line.strip().upper() == "<END>"
                )
            except ValueError:
                # <END> is an optional keyword to demarcate the end of the data.
                # If not present the end of the document is assumed to be the end
                # end of the data.
                end_line = len(lines) - 1

            self.data_lines = self.process_lines(lines[start_line:end_line])

        def str_vals(self):
            """WRITE: convert all values in table to a list of lists of strings.

            This sets the fill values and possibly column formats from the input
            formats={} keyword, then ends up calling table.pprint._pformat_col_iter()
            by a circuitous path. That function does the real work of formatting.
            Finally replace anything matching the fill_values.

            Returns
            -------
            values : list of list of str
            """
            self._set_fill_values(self.cols)
            self._set_col_formats()
            for col in self.cols:
                if np.issubdtype(col.dtype, np.integer):
                    if np.any(col.data > 2**31 - 1) or np.any(col.data < -(2**31)):
                        raise TdatFormatError(
                            "Values cannot be converted to a TDAT compatible integer."
                        )
                col.str_vals = []
                for val in col.info.iter_str_vals():
                    col.str_vals.append(val.replace("|", "\\|"))

            self._replace_vals(self.cols)
            return [col.str_vals for col in self.cols]

        def get_str_vals(self):
            """
            READ: Override the default get_str_vals to handle TDAT delimiters.
            """
            # if we have  delimiter from the header, use it.
            field_delimiter = getattr(self.header, "delimiter", self.splitter.delimiter)
            self.splitter.delimiter = field_delimiter
            record_delimiter = getattr(
                self.header, "record_delimiter", self.splitter.record_delimiter
            )
            allowed_delimiters = list(map(chr, range(1, 128)))
            if record_delimiter is not None:
                if record_delimiter not in allowed_delimiters:
                    raise TdatFormatError(
                        f"Unsupported record delimiter: {record_delimiter}."
                    )
                if record_delimiter == "|":
                    raise TdatFormatError("'|' character reserved for field delimiter.")
            for c in field_delimiter:
                if c not in allowed_delimiters:
                    raise TdatFormatError(f"Unsupported field delimiter: {c}.")
            nlines = len(self.header._line_fields)
            return self.splitter(self.data_lines, field_delimiter, record_delimiter, nlines)

        def write(self, lines):
            """
            WRITE: Override the default write to include <DATA> and <END> lines.
            """
            lines.append("<DATA>")
            super().write(lines)
            lines.append("<END>")

    class TdatOutputter(core.TableOutputter):
        """
        Output the table as an astropy.table.Table object.
        """

        def __call__(self, cols, meta):
            """
            READ: Override the default outputter.
            TDAT files may (optionally) specify which field lines should be used as
            the primary index and secondary indices Astropy tables support adding
            indices after creation. This overwrite adds labeled indices on read.
            """
            # Sets col.data to numpy array and col.type to io.ascii Type class (e.g.
            # FloatType) for each col.
            self._convert_vals(cols)

            t_cols = [
                np.ma.MaskedArray(x.data, mask=x.mask)
                if hasattr(x, "mask") and np.any(x.mask)
                else x.data
                for x in cols
            ]
            out = core.Table(t_cols, names=[x.name for x in cols], meta=meta["table"])
            indices = []
            for col, out_col in zip(cols, out.columns.values()):
                for attr in ("format", "unit", "description"):
                    if hasattr(col, attr):
                        setattr(out_col, attr, getattr(col, attr))
                if hasattr(col, "meta"):
                    out_col.meta.update(col.meta)
                    if "index" in col.meta:
                        if col.meta["index"] == "key":
                            indices.insert(0, col.name)
                        else:
                            indices.append(col.name)
            # Add indices, if specified
            if len(indices) > 0:
                for name in indices:
                    out.add_index(name)
            return out

    class Tdat(core.BaseReader):
        """TDAT format

        See: https://heasarc.gsfc.nasa.gov/docs/software/dbdocs/tdat.html

        Example::

          <HEADER>
          # # and // are comments
          table_name = example_table
          table_description = "Example table"
          #
          # Table Parameters
          #
          field[id] = integer [meta.id] (key) // Unique ID
          field[ra] = float:.4f_degree [pos.eq.ra] (index) // Right Ascension
          field[name] = char12 [meta.id] // Name
          #
          # Virtual Parameters
          #
          table_author = Example et al.
          #
          # Data Format Specification
          #
          line[1] = id name ra
          <DATA>
          1|TargetOne|1.0|
          2|TargetTwo|2.0|
          <END>

        The comments and keywords defined in the header, excepting common header
        section titles and blank comments, are available via the output table
        ``meta`` attribute::

          >>> from astropy.io import ascii
          >>> lines = ascii.tdat.make_example_data()
          >>> data = ascii.read(lines, format='tdat')
          >>> print(data.meta['comments'])
          ['# and // are comments']
          >>> for name, keyword in data.meta['keywords'].items():
          ...   print(name, keyword)
          table_name example_table
          table_description Example table
          table_author Example et al.

        When writing to the TDAT format, the header will be auto-populated by
        information in the Table, prioritizing information given in the Table.meta:

        **comments** : list or string, (optional)
          Table information which provide context. This information is
          included in the header preceding all other lines and commented
          out with #

        **keywords** : dict, (optional, recommended)
          Header keywords which will appear in the file as "name=value" lines.
          Of particular importance are table_name, table_description,
          and table_document_url.

        If there is no Table.meta, this writer will attempt to automatically
        generate the appropriate header information based on the table and
        column properties and the recommendations for the TDAT format by HEASARC.
        Column ``units`` are written using the CDS format.

        Example::

          >>> from astropy.table import Table
          >>> import sys
          >>> t = Table(names=('reference_id', 'RA', 'Name'),
          ...           data=[[1, 2, 3], [1.0, 2.0, 3.0], ['c', 'd', 'e']])
          >>> t.meta['table_name'] = "astropy_table"
          >>> t.write(sys.stdout, format="ascii.tdat")
          <HEADER>
          table_name = astropy_table
          #
          # Table Parameters
          #
          field[reference_id] = int4
          field[RA] = float8
          field[Name] = char1
          #
          # Data Format Specification
          #
          line[1] = reference_id RA Name
          #
          <DATA>
          1|1.0|c|
          2|2.0|d|
          3|3.0|e|
          <END>

        Including relevant metadata for the table and columns separately
        is possible with a mixture of attribute assignment and additions to the
        metadata::

          >>> from astropy.table import Table
          >>> from io import StringIO
          >>> t = Table(names=('reference_id', 'RA', 'Name'),
          ...           data=[[1, 2, 3], [1.0, 2.0, 3.0], ['c', 'd', 'e']])
          >>> t.meta["table_name"] = "example_table"
          >>> t.meta["table_description"] = "An example table for the tdat writer."
          >>> t.add_index('reference_id')
          >>> t.columns['reference_id'].meta['comment'] = "For internal reference only"
          >>> t.add_index('RA')
          >>> t.columns['RA'].unit = "degree"
          >>> t.columns['RA'].format = ".4f"
          >>> t.columns['RA'].meta['ucd'] = "pos.eq.ra"
          >>> t.columns['Name'].description = "The name of the source (if available)"
          >>> t.write(sys.stdout, format="ascii.tdat")
          <HEADER>
          table_name = example_table
          table_description = An example table for the tdat writer.
          #
          # Table Parameters
          #
          field[reference_id] = int4 (key) // // For internal reference only
          field[RA] = float8:.4f_deg [pos.eq.ra] (index)
          field[Name] = char1 // The name of the source (if available)
          #
          # Data Format Specification
          #
          line[1] = reference_id RA Name
          #
          <DATA>
          1|1.0000|c|
          2|2.0000|d|
          3|3.0000|e|
          <END>
        """

        _format_name = "tdat"
        _description = "HEASARC tdat format"
        _io_registry_can_write = True
        _io_registry_suffix = ".tdat"

        header_class = TdatHeader
        data_class = TdatData
        outputter_class = TdatOutputter