# Source code for spacepy.pycdf.istp

    #!/usr/bin/env python

    """Support for ISTP-compliant CDFs

    The `ISTP metadata standard  <https://spdf.gsfc.nasa.gov/sp_use_of_cdf.html>`_
    specifies the interpretation of the attributes in a CDF to describe
    relationships between the variables and their physical interpretation.

    This module supports that subset of CDFs.

    Authors: Jon Niehof

    Additional Contributors: Lorna Ellis, Asher Merrill

    Institution: University of New Hampshire

    Contact: Jonathan.Niehof@unh.edu

    .. rubric:: Classes

    .. autosummary::
        :toctree: autosummary
        :template: clean_class.rst

        FileChecks
        VariableChecks

    .. rubric:: Functions

    .. autosummary::
        :toctree: autosummary

        fillval
        format
    """

    import datetime
    import math
    import os.path
    import re

    import numpy
    import spacepy.datamodel
    import spacepy.pycdf
    import spacepy.pycdf.const

    class VariableChecks(object):
        """ISTP compliance checks for a single variable.

        Checks a variable's compliance with ISTP standards. This mostly
        performs checks that are not currently performed by the `ISTP
        skeleton editor <https://spdf.gsfc.nasa.gov/skteditor/>`_.  All
        tests return a list, one error string for every noncompliance
        found (empty list if compliant). :meth:`all` will perform all
        tests and concatenate all errors.

        .. autosummary::

            all
            depends
            depsize
            fieldnam
            recordcount
            validdisplaytype
            validrange
            validscale

        .. automethod:: all
        .. automethod:: depends
        .. automethod:: depsize
        .. automethod:: fieldnam
        .. automethod:: recordcount
        .. automethod:: validdisplaytype
        .. automethod:: validrange
        .. automethod:: validscale

        """
        #When adding new tests, add to list above, and the list in all()
        #Validation failures should be formatted as a sentence (initial cap,
        #closing period) and NOT include the variable name.

        @classmethod
        def all(cls, v, catch=False):
            """Perform all variable tests

            Parameters
            ----------
            v : :class:`~spacepy.pycdf.Var`
                Variable to check
            catch : bool
                Catch exceptions in tests (default False). If True, any
                exceptions in subtests will result in an addition to the
                validation failures of the form "Test x did not complete."
                Calling the individual test will reveal the full traceback.

            Returns
            -------
            list of str
                Description of each validation failure.

            Examples
            --------
            >>> import spacepy.pycdf
            >>> import spacepy.pycdf.istp
            >>> f = spacepy.pycdf.CDF('foo.cdf', create=True)
            >>> v = f.new('Var', data=[1, 2, 3])
            >>> spacepy.pycdf.istp.VariableChecks.all(v)
            ['No FIELDNAM attribute.']
            """
            #Update this list when adding new test functions
            callme = (cls.depends, cls.depsize, cls.fieldnam, cls.recordcount,
                      cls.validrange, cls.validscale, cls.validdisplaytype)
            errors = []
            for f in callme:
                try:
                    errors.extend(f(v))
                except:
                    if catch:
                        errors.append('Test {} did not complete.'.format(
                            f.__name__))
                    else:
                        raise
            return errors

        @classmethod
        def depends(cls, v):
            """Checks that DEPEND and LABL_PTR variables actually exist

            Check that variables specified in the variable attributes for
            `DEPEND
            <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#DEPEND_0>`_
            and `LABL_PTR
            <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#LABL_PTR_1>`_
            exist in the CDF.

            Parameters
            ----------
            v : :class:`~spacepy.pycdf.Var`
                Variable to check

            Returns
            -------
            list of str
                Description of each validation failure.

            """
            return ['{} variable {} missing'.format(a, v.attrs[a])
                    for a in v.attrs
                    if a.startswith(('DEPEND_', 'LABL_PTR_')) and
                    not v.attrs[a] in v.cdf_file]

        @classmethod
        def depsize(cls, v):
            """Checks that DEPEND has same shape as that dim

            Compares the size of variables specified in the variable
            attributes for `DEPEND
            <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#DEPEND_0>`_
            and compares to the size of the corresponding dimension in
            this variable.

            Parameters
            ----------
            v : :class:`~spacepy.pycdf.Var`
                Variable to check

            Returns
            -------
            list of str
                Description of each validation failure.

            """
            rv = int(v.rv()) #RV is a leading dimension
            errs = []
            # Check that don't have invalid DEPEND_1
            if v.shape == (0,):
                if 'DEPEND_1' in v.attrs or 'DEPEND_2' in v.attrs:
                    errs.append('Do not expect DEPEND_1 or DEPEND_2 in 1 dimensional variable.')
            for i in range(rv, len(v.shape)): #This is index on shape (of var)
                depidx = i + 1 - rv #This is x in  DEPEND_x
                target = v.shape[i]
                if not 'DEPEND_{}'.format(depidx) in v.attrs:
                    continue
                d = v.attrs['DEPEND_{}'.format(depidx)]
                if d in v.cdf_file:
                    dv = v.cdf_file[d]
                else:
                    continue #this is a different error
                if dv.rv() != ('DEPEND_0' in dv.attrs):
                    errs.append('DEPEND_{} {} is RV but has no DEPEND_0.'
                                .format(depidx, d))
                    continue
                #We hope the only weirdness is whether the dependency
                #is constant, or dependent on record. If it's dependent
                #on another dependency, this gets really weird really fast
                # If the dependency is dependent, remove the lower level
                # dependency size from consideration
                # eg. if counts [80,48], depends on energy [80,48],
                # depends on look [80], remove 80 from the view of energy
                # so that we accurately check 48==48.
                # NB: This assumes max of two layers of dependency
                if 'DEPEND_2' in dv.attrs:
                    errs.append('Do not expect three layers of dependency.')
                    continue
                elif 'DEPEND_1' in dv.attrs:
                    dd = dv.attrs['DEPEND_1']
                    if dd in v.cdf_file:
                        ddv = v.cdf_file[dd]
                    else:
                        continue #this is a different error
                    actual = list(dv.shape)
                    for ii in actual:
                        if ii in ddv.shape:
                            actual.remove(ii)
                    if 'DEPEND_0' in dv.attrs:
                        # record varying
                        dd = dv.attrs['DEPEND_0']
                        if dd[:5] != 'Epoch':
                            errs.append('Expect DEPEND_0 to be Epoch.')
                            continue
                        if dd in v.cdf_file:
                            ddv = v.cdf_file[dd]
                        else:
                            continue #this is a different error
                        for ii in actual:
                            if ii in ddv.shape:
                                actual.remove(ii)

                    if len(actual) != 1:
                        errs.append('More complicated double dependency than taken into account.')
                        continue
                    else:
                        actual = actual[0]
                else:
                    actual = dv.shape[int(dv.rv())]
                if target != actual:
                    errs.append('Dim {} sized {} but DEPEND_{} {} sized {}.'.format(
                        i, target, depidx, d, actual))

            return errs

        @classmethod
        def recordcount(cls, v):
            """Check that the DEPEND_0 has same record count as variable

            Checks the record count of the variable specified in the
            variable attribute for `DEPEND_0
            <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#DEPEND_0>`_
            and compares to the record count for this variable.

            Parameters
            ----------
            v : :class:`~spacepy.pycdf.Var`
                Variable to check

            Returns
            -------
            list of str
                Description of each validation failure.

            """
            if not v.rv() or not 'DEPEND_0' in v.attrs:
                return []
            dep0 = v.attrs['DEPEND_0']
            if not dep0 in v.cdf_file: #This is a DIFFERENT error
                return []
            if len(v) != len(v.cdf_file[dep0]):
                return ['{} records; DEPEND_0 {} has {}.'.format(
                    len(v), dep0, len(v.cdf_file[dep0]))]
            return []

        @classmethod
        def _validhelper(cls, v, rng=True):
            """Helper function for checking SCALEMIN/MAX, VALIDMIN/MAX

            Parameters
            ----------
            v : :class:`~spacepy.pycdf.Var`
                Variable to check

            rng : bool
                Do range check (True, default) or scale check (False)

            Returns
            -------
            list of str
                Description of each validation failure.
            """
            validscale = 'VALID' if rng else 'SCALE'
            whichmin, whichmax = ('VALIDMIN', 'VALIDMAX') if rng \
                                 else ('SCALEMIN', 'SCALEMAX')
            errs = []
            vshape = v.shape
            minval, maxval = spacepy.pycdf.lib.get_minmax(v.type())
            if rng:
                data = v[...]
                if 'FILLVAL' in v.attrs:
                    if numpy.issubdtype(v.dtype, numpy.float):
                        is_fill = numpy.isclose(data, v.attrs['FILLVAL'])
                    else:
                        is_fill = data == v.attrs['FILLVAL']
                else:
                    is_fill = numpy.zeros(shape=vshape, dtype=numpy.bool)
            for which in (whichmin, whichmax):
                if not which in v.attrs:
                    continue
                attrval = v.attrs[which]
                multidim = bool(numpy.shape(attrval)) #multi-dimensional
                if multidim: #Compare shapes, require only 1D var
                    #Match attribute dim to first non-record var dim
                    firstdim = int(v.rv())
                    if vshape[firstdim] != numpy.shape(attrval)[0]:
                        errs.append(('{} element count {} does not match first data'
                                     ' dimension size {}.').format(
                                         which, numpy.shape(attrval)[0],
                                         v.shape[firstdim]))
                        continue
                    if len(vshape) != firstdim + 1: #only one non-record dim
                        errs.append('Multi-element {} only valid with 1D variable.'
                                    .format(which))
                        continue
                    if firstdim: #Add pseudo-record dim
                        attrval = numpy.reshape(attrval, (1, -1))
                if numpy.any((attrval < minval)) or numpy.any((attrval > maxval)):
                    errs.append('{} ({}) outside data range ({},{}).'.format(
                        which, attrval[0, :] if multidim else attrval,
                        minval, maxval))
                if not rng or not len(v): #nothing to compare
                    continue
                #Always put numpy array on the left so knows to do element compare
                idx = (data < attrval) if which == whichmin \
                      else (data > attrval)
                idx = numpy.logical_and(idx, numpy.logical_not(is_fill))
                if idx.any():
                    direction = 'under' if which == whichmin else 'over'
                    if len(vshape) == 0: #Scalar
                        errs.append('Value {} {} {} {}.'.format(
                            data, direction, which,
                            attrval[0, :] if multidim else attrval))
                        continue
                    badidx = numpy.nonzero(idx)
                    badvals = data[badidx]
                    if len(badidx) > 1: #Multi-dimensional data
                        badidx = numpy.transpose(badidx) #Group by value not axis
                    else:
                        badidx = badidx[0] #Just recover the index value
                    if len(badvals) < 10:
                        badvalstr = ', '.join(str(d) for d in badvals)
                        badidxstr = ', '.join(str(d) for d in badidx)
                        errs.append('Value {} at index {} {} {} {}.'.format(
                            badvalstr, badidxstr,
                            direction, which,
                            attrval[0, :] if multidim else attrval))
                    else:
                        errs.append('{} values {} {} {}'.format(
                            len(badvals), direction, which,
                            attrval[0, :] if multidim else attrval))
            if (whichmin in v.attrs) and (whichmax in v.attrs):
                if numpy.any(v.attrs[whichmin] > v.attrs[whichmax]):
                    errs.append('{} > {}.'.format(whichmin, whichmax))
            return errs

        @classmethod
        def validrange(cls, v):
            """Check that all values are within VALIDMIN/VALIDMAX, or FILLVAL

            Compare all values of this variable to `VALIDMIN
            <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#VALIDMIN>`_
            and ``VALIDMAX``; fails validation if any values are below
            VALIDMIN or above ``VALIDMAX`` unless equal to `FILLVAL
            <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#FILLVAL>`_.

            Parameters
            ----------
            v : :class:`~spacepy.pycdf.Var`
                Variable to check

            Returns
            -------
            list of str
                Description of each validation failure.

            """
            return cls._validhelper(v)

        @classmethod
        def validscale(cls, v):
            """Check SCALEMIN<=SCALEMAX, and both in range for CDF datatype.

            Compares `SCALEMIN
            <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#SCALEMIN>`_
            to ``SCALEMAX`` to make sure it isn't larger and both are
            within range of the variable CDF datatype.

            Parameters
            ----------
            v : :class:`~spacepy.pycdf.Var`
                Variable to check

            Returns
            -------
            list of str
                Description of each validation failure.

            """
            return cls._validhelper(v, False)

        @classmethod
        def validdisplaytype(cls, v):
            """Check that plottype matches dimensions.

            Check `DISPLAYTYPE
            <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#DISPLAY_TYPE>`_
            of this variable and makes sure it is reasonable for the
            variable dimensions.

            Parameters
            ----------
            v : :class:`~spacepy.pycdf.Var`
                Variable to check

            Returns
            -------
            list of str
                Description of each validation failure.

            """
            time_st = 'time_series'
            spec_st = 'spectrogram'
            errs = []
            if 'DISPLAY_TYPE' in v.attrs:
                if (len(v.shape) == 1) and (v.attrs['DISPLAY_TYPE'] != time_st):
                    errs.append('1 dim variable with {} display type.'.format(
                        v.attrs['DISPLAY_TYPE']))
                elif (len(v.shape) > 1) and (v.attrs['DISPLAY_TYPE'] != spec_st):
                    errs.append('Multi dim variable with {} display type.'.format(
                        v.attrs['DISPLAY_TYPE']))
            return errs

        @classmethod
        def fieldnam(cls, v):
            """Check that FIELDNAM attribute matches variable name.

            Compare `FIELDNAM
            <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#FIELDNAM>`_
            attribute to the variable name; fail validation if they don't
            match.

            Parameters
            ----------
            v : :class:`~spacepy.pycdf.Var`
                Variable to check

            Returns
            -------
            list of str
                Description of each validation failure.

            """
            errs = []
            vname = v.name()
            if 'FIELDNAM' not in v.attrs:
                errs.append('No FIELDNAM attribute.')
            elif v.attrs['FIELDNAM'] != vname:
                errs.append('FIELDNAM attribute {} does not match var name.'
                            .format(v.attrs['FIELDNAM']))
            return errs

    class FileChecks(object):
        """ISTP compliance checks for a CDF file.

        Checks a file's compliance with ISTP standards. This mostly
        performs checks that are not currently performed by the `ISTP
        skeleton editor <https://spdf.gsfc.nasa.gov/skteditor/>`_.  All
        tests return a list, one error string for every noncompliance
        found (empty list if compliant). :meth:`all` will perform all
        tests and concatenate all errors.

        .. autosummary::

            all
            filename
            time_monoton
            times

        .. automethod:: all
        .. automethod:: filename
        .. automethod:: time_monoton
        .. automethod:: times

        """
        #When adding new tests, add to list above, and the list in all()
        #Validation failures should be formatted as a sentence (initial cap,
        #closing period).

        @classmethod
        def all(cls, f, catch=False):
            """Perform all variable and file-level tests

            In addition to calling every test in this class, will also call
            :meth:`VariableChecks.all` for every variable in the file.

            Parameters
            ----------
            f : :class:`~spacepy.pycdf.CDF`
                Open CDF file to check
            catch : bool
                Catch exceptions in tests (default False). If True, any
                exceptions in subtests will result in an addition to the
                validation failures of the form "Test x did not complete."
                Calling the individual test will reveal the full traceback.

            Returns
            -------
            list of str
                Description of each validation failure.

            Examples
            --------
            >>> import spacepy.pycdf
            >>> import spacepy.pycdf.istp
            >>> f = spacepy.pycdf.CDF('foo.cdf', create=True)
            >>> v = f.new('Var', data=[1, 2, 3])
            >>> spacepy.pycdf.istp.FileChecks.all(f)
            ['No Logical_source in global attrs.',
            'No Logical_file_id in global attrs.',
            'Cannot parse date from filename foo.cdf.',
            'Var: No FIELDNAM attribute.']
            """
            #Update this list when adding new test functions
            callme = (cls.filename, cls.time_monoton, cls.times,)
            errors = []
            for func in callme:
                try:
                    errors.extend(func(f))
                except:
                    if catch:
                        errors.append('Test {} did not complete.'.format(
                            f.__name__))
                    else:
                        raise

            for v in f:
                errors.extend(('{}: {}'.format(v, e)
                               for e in VariableChecks.all(f[v], catch=catch)))
            return errors

        @classmethod
        def filename(cls, f):
            """Compare filename to global attributes

            Check global attribute `Logical_file_id
            <https://spdf.gsfc.nasa.gov/istp_guide/gattributes.html#Logical_file_id>`_
            and `Logical_source
            <https://spdf.gsfc.nasa.gov/istp_guide/gattributes.html#Logical_source>`_
            for consistency with CDF filename.

            Parameters
            ----------
            f : :class:`~spacepy.pycdf.CDF`
                Open CDF file to check

            Returns
            -------
            list of str
                Description of each validation failure.

            """
            errs = []
            for a in ('Logical_source', 'Logical_file_id'):
                if not a in f.attrs or len(f.attrs[a]) == 0:
                    errs.append('No {} in global attrs.'.format(a))
            if errs:
                return errs
            fname = os.path.basename(f.pathname)
            if not bytes is str:
                fname = fname.decode('ascii')
            if not fname.startswith(f.attrs['Logical_source'][0]):
                errs.append("Logical_source {} doesn't match filename {}.".format(
                    f.attrs['Logical_source'][0], fname))
            if fname[:-4] != f.attrs['Logical_file_id'][0]:
                errs.append("Logical_file_id {} doesn't match filename {}.".format(
                    f.attrs['Logical_file_id'][0], fname))
            return errs

        @classmethod
        def time_monoton(cls, f):
            """Checks that times are monotonic

            Check that all `Epoch
            <https://spdf.gsfc.nasa.gov/istp_guide/variables.html#support_data_eg1>`_
            variables are monotonically increasing.

            Parameters
            ----------
            f : :class:`~spacepy.pycdf.CDF`
                Open CDF file to check

            Returns
            -------
            list of str
                Description of each validation failure.

            """
            errs = []
            for v in f:
                if not f[v].type() in (spacepy.pycdf.const.CDF_EPOCH.value,
                                       spacepy.pycdf.const.CDF_EPOCH16.value,
                                       spacepy.pycdf.const.CDF_TIME_TT2000.value):
                    continue
                data = f[v][...]
                idx = numpy.where(numpy.diff(data) < datetime.timedelta(0))[0]
                if not any(idx):
                    continue
                errs.append('{}: Nonmonotonic time at record {}.'.format(
                    v, ', '.join((str(i) for i in (idx + 1)))))
            return errs

        @classmethod
        def times(cls, f):
            """Compare filename to times

            Check that all `Epoch
            <https://spdf.gsfc.nasa.gov/istp_guide/variables.html#support_data_eg1>`_
            variables only contain times matching filename.

            Parameters
            ----------
            f : :class:`~spacepy.pycdf.CDF`
                Open CDF file to check

            Returns
            -------
            list of str
                Description of each validation failure.

            Notes
            -----
            This function assumes daily files and should be extended based on the
            File_naming_convention global attribute (which itself is another good
            check to have.)

            """
            errs = []
            fname = os.path.basename(f.pathname)
            if not bytes is str:
                fname = fname.decode('ascii')
            m = re.search('\d{8}', fname)
            if not m:
                return ['Cannot parse date from filename {}'.format(fname)]
            datestr = m.group(0)
            for v in f:
                if f[v].type() in (spacepy.pycdf.const.CDF_EPOCH.value,
                                   spacepy.pycdf.const.CDF_EPOCH16.value,
                                   spacepy.pycdf.const.CDF_TIME_TT2000.value):
                    datestrs = list(set((d.strftime('%Y%m%d') for d in f[v][...])))
                    if len(datestrs) == 0:
                        continue
                    elif len(datestrs) > 1:
                        errs.append('{}: multiple days {}.'.format(
                            v, ', '.join(sorted(datestrs))))
                    elif datestrs[0] != datestr:
                        errs.append('{}: date {} doesn\'t match file {}.'.format(
                            v, datestrs[0], fname))
            return errs

    def fillval(v):
        """Set ISTP-compliant FILLVAL on a variable

        Sets a CDF variable's `FILLVAL
        <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#FILLVAL>`_
        attribute to the value required by ISTP (based on variable type).

        Parameters
        ----------
        v : :class:`~spacepy.pycdf.Var`
            CDF variable to update

        Examples
        --------
        >>> import spacepy.pycdf
        >>> import spacepy.pycdf.istp
        >>> f = spacepy.pycdf.CDF('foo.cdf', create=True)
        >>> v = f.new('Var', data=[1, 2, 3])
        >>> spacepy.pycdf.istp.fillval(v)
        >>> v.attrs['FILLVAL']
        -128
        """
        #Fill value, indexed by the CDF type (numeric)
        fillvals = {}
        #Integers
        for i in (1, 2, 4, 8):
            fillvals[getattr(spacepy.pycdf.const, 'CDF_INT{}'.format(i)).value] = \
                - 2 ** (8*i - 1)
            if i == 8:
                continue
            fillvals[getattr(spacepy.pycdf.const, 'CDF_UINT{}'.format(i)).value] = \
                2 ** (8*i) - 1
        fillvals[spacepy.pycdf.const.CDF_EPOCH16.value] = (-1e31, -1e31)
        fillvals[spacepy.pycdf.const.CDF_REAL8.value] = -1e31
        fillvals[spacepy.pycdf.const.CDF_REAL4.value] = -1e31
        fillvals[spacepy.pycdf.const.CDF_CHAR.value] = ' '
        fillvals[spacepy.pycdf.const.CDF_UCHAR.value] = ' '
        #Equivalent pairs
        for cdf_t, equiv in (
                (spacepy.pycdf.const.CDF_TIME_TT2000, spacepy.pycdf.const.CDF_INT8),
                (spacepy.pycdf.const.CDF_EPOCH, spacepy.pycdf.const.CDF_REAL8),
                (spacepy.pycdf.const.CDF_BYTE, spacepy.pycdf.const.CDF_INT1),
                (spacepy.pycdf.const.CDF_FLOAT, spacepy.pycdf.const.CDF_REAL4),
                (spacepy.pycdf.const.CDF_DOUBLE, spacepy.pycdf.const.CDF_REAL8),
        ):
            fillvals[cdf_t.value] = fillvals[equiv.value]
        if 'FILLVAL' in v.attrs:
            del v.attrs['FILLVAL']
        v.attrs.new('FILLVAL', data=fillvals[v.type()], type=v.type())

    def format(v, use_scaleminmax=False, dryrun=False):
        """Set ISTP-compliant FORMAT on a variable

        Sets a CDF variable's `FORMAT
        <https://spdf.gsfc.nasa.gov/istp_guide/vattributes.html#FORMAT>`_
        attribute, which provides a Fortran-like format string that should
        be useable for printing any valid value in the variable. Sets
        according to the VALIDMIN/VALIDMAX attributes (or, optionally,
        SCALEMIN/SCALEMAX) if present, otherwise uses the full range of
        the type.

        Parameters
        ----------
        v : :class:`~spacepy.pycdf.Var`
            Variable to update
        use_scaleminmax : bool, optional
            Use SCALEMIN/MAX instead of VALIDMIN/MAX (default False).
            Note: istpchecks may complain about result.
        dryrun : bool, optional
            Print the decided format to stdout instead of modifying
            the CDF (for use in command-line debugging) (default False).

        Examples
        --------
        >>> import spacepy.pycdf
        >>> import spacepy.pycdf.istp
        >>> f = spacepy.pycdf.CDF('foo.cdf', create=True)
        >>> v = f.new('Var', data=[1, 2, 3])
        >>> spacepy.pycdf.istp.format(v)
        >>> v.attrs['FORMAT']
        'I4'

        """
        if use_scaleminmax:
            minn = 'SCALEMIN'
            maxx = 'SCALEMAX'
        else:
            minn = 'VALIDMIN'
            maxx = 'VALIDMAX'
        cdftype = v.type()
        if cdftype in (spacepy.pycdf.const.CDF_INT1.value,
                       spacepy.pycdf.const.CDF_INT2.value,
                       spacepy.pycdf.const.CDF_INT4.value,
                       spacepy.pycdf.const.CDF_INT8.value,
                       spacepy.pycdf.const.CDF_UINT1.value,
                       spacepy.pycdf.const.CDF_UINT2.value,
                       spacepy.pycdf.const.CDF_UINT4.value,
                       spacepy.pycdf.const.CDF_BYTE.value):
            if minn in v.attrs: #Just use validmin or scalemin
                minval = v.attrs[minn]
            elif cdftype in (spacepy.pycdf.const.CDF_UINT1.value,
                             spacepy.pycdf.const.CDF_UINT2.value,
                             spacepy.pycdf.const.CDF_UINT4.value): #unsigned, easy
                minval = 0
            elif cdftype == spacepy.pycdf.const.CDF_BYTE.value:
                minval = - 2 ** 7
            else: #Signed, harder
                size = next((i for i in (1, 2, 4, 8) if getattr(
                    spacepy.pycdf.const, 'CDF_INT{}'.format(i)).value == cdftype))
                minval = - 2 ** (8*size  - 1)
            if maxx in v.attrs: #Just use max
                maxval = v.attrs[maxx]
            elif cdftype == spacepy.pycdf.const.CDF_BYTE.value:
                maxval = 2 ** 7 - 1
            else:
                size = next((8 * i for i in (1, 2, 4) if getattr(
                    spacepy.pycdf.const, 'CDF_UINT{}'.format(i)).value == cdftype),
                            None)
                if size is None:
                    size = next((8 * i for i in (1, 2, 4, 8) if getattr(
                        spacepy.pycdf.const, 'CDF_INT{}'.format(i)).value ==
                                 cdftype)) - 1
                maxval = 2 ** size - 1
            #Two tricks:
            #-Truncate and add 1 rather than ceil so get
            #powers of 10 (log10(10) = 1 but needs two digits)
            #-Make sure not taking log of zero
            if minval < 0: #Need an extra space for the negative sign
                fmt = 'I{}'.format(int(math.log10(max(
                    abs(maxval), abs(minval), 1))) + 2)
            else:
                fmt = 'I{}'.format(int(
                    math.log10(maxval) if maxval != 0 else 1) + 1)
        elif cdftype == spacepy.pycdf.const.CDF_TIME_TT2000.value:
            fmt = 'A{}'.format(len('9999-12-31T23:59:59.999999999'))
        elif cdftype == spacepy.pycdf.const.CDF_EPOCH16.value:
            fmt = 'A{}'.format(len('31-Dec-9999 23:59:59.999.999.000.000'))
        elif cdftype == spacepy.pycdf.const.CDF_EPOCH.value:
            fmt = 'A{}'.format(len('31-Dec-9999 23:59:59.999'))
        elif cdftype in (spacepy.pycdf.const.CDF_REAL8.value,
                         spacepy.pycdf.const.CDF_REAL4.value,
                         spacepy.pycdf.const.CDF_FLOAT.value,
                         spacepy.pycdf.const.CDF_DOUBLE.value):
            # Prioritize SCALEMIN/MAX to find the number of decimals to include
            if 'SCALEMIN' in v.attrs and 'SCALEMAX' in v.attrs:
                range = v.attrs['SCALEMAX'] - v.attrs['SCALEMIN']
            # If not, use VALIDMIN/MAX
            elif 'VALIDMIN' in v.attrs and 'VALIDMAX' in v.attrs:
                range = v.attrs['VALIDMAX'] - v.attrs['VALIDMIN']
            # If not, just use nothing.
            else:
                range = None
            # Find how many spaces we need for the 'integer' part of the number
            # (Use maxx-minn for this...effectively uses VALIDMIN/MAX for most
            # cases.)
            if range and (minn in v.attrs and maxx in v.attrs):
                if len(str(int(v.attrs[maxx]))) >=\
                   len(str(int(v.attrs[minn]))):
                    ln = str(int(v.attrs[maxx]))
                else:
                    ln = str(int(v.attrs[minn]))
            if range and ln and range < 0: # Cover all our bases:
                # raise ValueError('Range ({} - {}) cannot be negative:'
                    # '\nVarname: {}\nRange: {}'.format(maxx, minn, v, range))
                ### Instead of throwing an error, just use None
                # There are old cases that for some reason have negative ranges, so
                # this is really more of a compatibility choice than a good
                # decision.
                range = None
            # All of the lengths below (the +4, +3, +2, etc...) should be EXACTLY
            # enough.  Consider adding 1, (4+1=5, 3+1=4, etc...) to possibly make
            # this easier.
            # elif range and ln and range <= 11: # If range <= 11, we want 2 decimal places:
            if range and ln and range <= 11: # If range <= 11, we want 2 decimal places:
                # Need extra for '.', and 3 decimal places (4 extra)
                fmt = 'F{}.3'.format(len([i for i in ln]) + 4)
            elif range and ln and 11 < range <= 101:
                # Need extra for '.' (1 extra)
                fmt = 'F{}.2'.format(len([i for i in ln]) + 3)
            elif range and ln and 101 < range <= 1000:
                # Need extra for '.' (1 extra)
                fmt = 'F{}.1'.format(len([i for i in ln]) + 2)
            else:
                # No range, must not be populated, copied from REAL4/8(s) above
                # OR we don't care because it's a 'big' number:
                fmt = 'G10.2E3'
        elif cdftype in (spacepy.pycdf.const.CDF_CHAR.value,
                         spacepy.pycdf.const.CDF_UCHAR.value):
            #This is a bit weird but pycdf consider nelems private. Should change.
            fmt = 'A{}'.format(v._nelems())
        else:
            raise ValueError("Couldn't find FORMAT for {} of type {}".format(
                v.name(),
                spacepy.pycdf.lib.cdftypenames.get(cdftype, 'UNKNOWN')))
        if dryrun:
            print(fmt)
        else:
            if 'FORMAT' in v.attrs:
                del v.attrs['FORMAT']
            v.attrs.new('FORMAT', data=fmt, type=spacepy.pycdf.const.CDF_CHAR)