Source code for julian.formatters

##########################################################################################
# julian/formatters.py
##########################################################################################
"""
==========
Formatters
==========
"""

import numpy as np
from julian.calendar       import ymd_from_day, yd_from_day
from julian.leap_seconds   import seconds_on_day
from julian.utc_tai_tdb_tt import day_sec_from_tai
from julian.time_of_day    import hms_from_sec
from julian._utils         import _float, _int, _is_float, _number

_DIGITS1 = np.array(['%d' % i for i in range(10)])
_BDIGITS1 = _DIGITS1.astype('S')

_DIGITS2 = np.array(['%02d' % i for i in range(100)])
_BDIGITS2 = _DIGITS2.astype('S')

_DIGITS3 = np.array(['%03d' % i for i in range(367)])
_BDIGITS3 = _DIGITS3.astype('S')

##########################################################################################
# Date formatting
##########################################################################################

[docs] def format_day(day, order='YMD', *, ydigits=4, dash='-', ddigits=None, proleptic=False, buffer=None, kind="U"): """Format a date or array of dates. Parameters: day (int, float, or array-like): Day number of relative to January 1, 2000. order (str): The order of the year, optional month, and day fields, one of "YMD", "MDY", "DMY", "YD", or "DY". ydigits (int, optional): Number of year digits to include in year, 2 or 4. dash (str) Character(s) to include between fields, if any. Default is "-". Use "" for no separators; any other string can also be used in place of the dashes. ddigits (int, optional): Decimal digits to include in day values; use -1 or None to suppress the decimal point; ignored if day values are integers. proleptic (bool, optional): True to interpret all dates according to the modern Gregorian calendar, even those that occurred prior to the transition from the Julian calendar. False to use the Julian calendar for earlier dates. buffer (array[str or bytes]): An optional array of strings or byte strings into which to write the results. Must have sufficient dimensions. This can be either strings (dtype.kind = "U") or bytes (dtype.kind = "S"); if the latter, you can provide a NumPy memmap as input and this function will write content directly into an ASCII table file. kind (str): "U" to return strings, "S" to return bytes. Ignored if `buffer` is provided. Returns: str or array: The formatted date(s). """ if order not in ('YMD', 'MDY', 'DMY', 'YD', 'DY'): raise ValueError('unrecognized date format order: ' + repr(order)) if ydigits not in (2, 4): raise ValueError('ydigits must equal 2 or 4') has_dot = _is_float(day) and ddigits is not None and ddigits >= 0 if has_dot: scale = 10**ddigits day = ((_float(day) * scale + 0.5)//1 + 0.1)/scale # +0.1 to ensure there is no rounding down after division by scale int_day = _int(day) frac = day - int_day day = int_day else: day = _int(day) frac = None shape = np.shape(day) # Interpret the buffer and kind if buffer is None: if kind not in ('U', 'S'): raise ValueError('invalid kind, must be "U" or "S"') su = '=U' if kind == 'U' else '|S' else: kind = buffer.dtype.kind su = buffer.dtype.byteorder + kind if kind not in ('U', 'S'): raise ValueError('invalid buffer; kind must be "U" or "S"') if shape: if shape[:-1] != buffer.shape[:-1]: raise ValueError('buffer shape does not match that of date array') if buffer.shape[-1] < shape[-1]: raise ValueError('buffer shape is too small for the date array') if kind == 'U': w = 4 # output itemsize null = '' # content of an empty cell dot = '.' # representation for a period dash_ = dash # representation for the field separator vals1 = _DIGITS1 # representations of numbers 0-9 vals2 = _DIGITS2 # representations of numbers 0-99 vals3 = _DIGITS3 # representations of numbers 0-366 else: w = 1 null = b'\0' dot = b'.' dash_ = dash.encode('latin8') vals1 = _BDIGITS1 vals2 = _BDIGITS2 vals3 = _BDIGITS3 # Translate the days; determine the string format and dtype fmt_list = [] dtype_dict = {} lstring = 0 ldash = len(dash) for field, c in enumerate(order): if field > 0 and ldash: fmt_list.append(dash.replace('{', '{{').replace('}', '}}')) dtype_dict['dash' + str(field)] = (su + str(ldash), lstring * w) lstring += ldash if c == 'Y': fmt_list.append('{y:0' + str(ydigits) + 'd}') if ydigits == 4: dtype_dict['y12'] = (su + '2', lstring * w) lstring += 2 dtype_dict['y34'] = (su + '2', lstring * w) lstring += 2 elif c == 'M': fmt_list.append('{m:02d}') dtype_dict['m'] = (su + '2', lstring * w) lstring += 2 else: # c == 'D' dlen = 3 if len(order) == 2 else 2 fmt_list.append('{d:0' + str(dlen) + 'd}') dtype_dict['d'] = (su + str(dlen), lstring * w) lstring += dlen if has_dot: fmt_list.append('.') dtype_dict['dot'] = (su + '1', lstring * w) lstring += 1 if ddigits > 0: fmt_list.append('{f:0' + str(ddigits) + 'd}') for i in range(ddigits): dtype_dict['f' + str(i)] = (su + '1', lstring * w) lstring += 1 fmt = ''.join(fmt_list) # Convert to y,m,d if len(order) == 3: (y, m, d) = ymd_from_day(day, proleptic=proleptic) else: (y, d) = yd_from_day(day, proleptic=proleptic) m = 0 # will be ignored # Use string formatting for a scalar return without a buffer if not shape and buffer is None: if ydigits == 2: y = y % 100 if has_dot and ddigits > 0: f = int((frac * 10.**ddigits) // 1) else: f = 0 result = fmt.format(y=y, m=m, d=d, f=f) if kind == 'U': return result else: return result.encode('latin8') # Create a buffer if necessary; otherwise, check dimensions if buffer is None: buffer = np.empty(shape, dtype='=' + kind + str(lstring)) else: if lstring * w > buffer.dtype.itemsize: raise ValueError('buffer itemsize is too small for the date format') if lstring * w < buffer.dtype.itemsize: extra = buffer.dtype.itemsize//w - lstring dtype_dict['extra'] = (su + str(extra), lstring * w) # Fill in the fields buffer.fill(null) view = buffer.view(np.dtype(dtype_dict)) if shape: view = view[..., :shape[-1]] if 'y12' in dtype_dict: view['y12'] = vals2[y // 100] view['y34'] = vals2[y % 100] if len(order) == 3: view['m'] = vals2[m] view['d'] = vals2[d] else: view['d'] = vals3[d] if 'dash1' in dtype_dict: view['dash1'] = dash_ if 'dash2' in dtype_dict: view['dash2'] = dash_ if 'dot' in dtype_dict: view['dot'] = dot if frac is not None: for i in range(ddigits): frac *= 10 f = (frac // 1).astype('int') frac -= f view['f' + str(i)] = vals1[f] return buffer
########################################################################################## # Time of day formatting ##########################################################################################
[docs] def format_sec(sec, digits=None, *, colon=':', suffix='', buffer=None, kind='U'): """A time of day in seconds converted to "hh:mm:ss[.fff][Z]" or similar formats. This function supports scalar or array-like inputs. If the latter, an array of strings or ASCII bytes is returned. Note that the optional output buffer can be either strings (dtype.kind = "U") or bytes (dtype.kind = "S"). If the latter, you can provide a NumPy memmap as input and this function will write content directly into an ASCII table file. Parameters: sec (int, float, or array-like): Elapsed seconds into day. Each value should be >= 0 and < 86410. digits (int, optional): Decimal digits to include; use -1 or None to suppress the decimal point; ignored if `sec` values are integers. colon (str): Character(s) to include between fields, if any. Default is ":". Use "" for no separators; any other string can also be used in place of the colons. suffix (str, optional): "Z" to include the Zulu time zone indicator. buffer (array[str or bytes]): An optional array of strings or byte strings into which to write the results. Must have sufficient dimensions. This can be either strings (dtype.kind = "U") or bytes (dtype.kind = "S"); if the latter, you can provide a NumPy memmap as input and this function will write content directly into an ASCII table file. kind (str): "U" to return strings, "S" to return bytes. Ignored if `buffer` is provided. Returns: str or array: The formatted time(s). """ # Convert secs to h,m,s sec = _number(sec) shape = np.shape(sec) (h, m, s) = hms_from_sec(sec, validate=True, leapsecs=True) has_dot = digits is not None and digits >= 0 if has_dot: scale = 10**digits sec = ((_float(sec) * scale + 0.5)//1 + 0.1)/scale # +0.1 to ensure there is no rounding down after division by scale int_sec = _int(sec) frac = sec - int_sec sec = int_sec else: s = _int(s) frac = None # Interpret the buffer and kind if buffer is None: if kind not in ('U', 'S'): raise ValueError('invalid kind, must be "U" or "S"') su = '=U' if kind == 'U' else '|S' else: kind = buffer.dtype.kind su = buffer.dtype.byteorder + kind if kind not in ('U', 'S'): raise ValueError('invalid buffer; kind must be "U" or "S"') if shape: if shape[:-1] != buffer.shape[:-1]: raise ValueError('buffer shape does not match that of time array') if buffer.shape[-1] < shape[-1]: raise ValueError('buffer shape is too small for the time array') if kind == 'U': w = 4 # output itemsize null = '' # content of an empty cell dot = '.' # representation of a period colon_ = colon # representation for the field separator vals1 = _DIGITS1 # representations of numbers 0-9 vals2 = _DIGITS2 # representations of numbers 0-99 else: w = 1 null = b'\0' dot = b'.' colon_ = colon.encode('latin8') vals1 = _BDIGITS1 vals2 = _BDIGITS2 # Determine the string format and dtype lcolon = len(colon) dtype_dict = { 'h': (su + '2', 0), 'm': (su + '2', (2 + lcolon) * w), 's': (su + '2', (4 + 2 * lcolon) * w), } if lcolon: dtype_dict['colon1'] = (su + str(lcolon), 2 * w) dtype_dict['colon2'] = (su + str(lcolon), (4 + lcolon) * w) lstring = 6 + 2 * lcolon fmt = '%02d' + colon + '%02d' + colon + '%02d' if has_dot: if digits == 0: fmt += '.' dtype_dict['dot'] = (su + '1', lstring * w) lstring += 1 else: fmt += '.%0' + str(digits) + 'd' dtype_dict['dot'] = (su + '1', lstring * w) lstring += 1 for i in range(digits): dtype_dict['f' + str(i)] = (su + '1', lstring * w) lstring += 1 lsuffix = len(suffix) if suffix: fmt += suffix dtype_dict['z'] = (su + str(lsuffix), lstring * w) lstring += lsuffix # 0-D return without a buffer is easy if not shape and buffer is None: if has_dot and digits > 0: f = int((frac * 10.**digits) // 1) if has_dot and digits > 0: result = fmt % (h, m, int(s), f) else: result = fmt % (h, m, int(s)) if kind == 'U': return result else: return result.encode('latin8') # Create a buffer if necessary; otherwise, check dimensions if buffer is None: buffer = np.empty(shape, dtype='=' + kind + str(lstring)) else: if lstring * w > buffer.dtype.itemsize: raise ValueError('buffer itemsize is too small for the ISO time format') if lstring * w < buffer.dtype.itemsize: extra = buffer.dtype.itemsize//w - lstring dtype_dict['extra'] = (su + str(extra), lstring * w) # Fill in the fields buffer.fill(null) view = buffer.view(np.dtype(dtype_dict)) if shape: view = view[..., :shape[-1]] int_s = _int(s) view['h'] = vals2[h] view['m'] = vals2[m] view['s'] = vals2[int_s] if lcolon: view['colon1'] = colon_ view['colon2'] = colon_ if 'dot' in dtype_dict: view['dot'] = dot if 'z' in dtype_dict: view['z'] = suffix if frac is not None: for i in range(digits): frac *= 10 f = (frac // 1).astype('int') frac -= f view['f' + str(i)] = vals1[f] return buffer
########################################################################################## # Date/time formatting ##########################################################################################
[docs] def format_day_sec(day, sec, order='YMDT', *, ydigits=4, dash='-', sep='T', colon=':', digits=None, suffix='', proleptic=False, buffer=None, kind='U'): """Format a date and time. This function supports scalar or array-like inputs. If array-like inputs are provided, an array of strings or ASCII byte strings is returned. Note that the optional output buffer can be either strings (dtype "U") or bytes (dtype "S"). If the latter, you can define it as a NumPy memmap and write content directly into an ASCII table file. Parameters: day (int, float, or array-like): Day number of relative to January 1, 2000. sec (int, float, or array-like): Elapsed seconds into day. order (str): The order of the year, optional month, day, and time fields. Can be any of "YMD", "MDY", "DMY", "YD", or "DY". Add "T" at the beginning or end to indicate whether times come before or after the date. ydigits (int, optional): Number of year digits to include in year, 2 or 4. dash (str) Character(s) to include between fields, if any. Default is "-". Use "" for no separators; any other string can also be used in place of the dashes. sep (str): Character(s) to appear between the date and the time. colon (str): Character(s) to include between fields, if any. Default is ":". Use "" for no separators; any other string can also be used in place of the colons. digits (int, optional): Decimal digits to include second values; use -1 or None to suppress the decimal point; ignored if `sec` values are integers. suffix (str, optional): "Z" to include the Zulu time zone indicator. proleptic (bool, optional): True to interpret all dates according to the modern Gregorian calendar, even those that occurred prior to the transition from the Julian calendar. False to use the Julian calendar for earlier dates. buffer (array[str or bytes]): An optional array of strings or byte strings into which to write the results. Must have sufficient dimensions. This can be either strings (dtype.kind = "U") or bytes (dtype.kind = "S"); if the latter, you can provide a NumPy memmap as input and this function will write content directly into an ASCII table file. kind (str): "U" to return strings, "S" to return bytes. Ignored if `buffer` is provided. Returns: str or array: The formatted date-time value(s). """ ymd_order = order.replace('T', '') if order[0] != 'T' and order[-1] != 'T': raise ValueError('"T" missing from order specification') day = _int(day) sec = _number(sec) day, sec = np.broadcast_arrays(day, sec) shape = np.shape(day) # Interpret the buffer and kind if buffer is None: if kind not in ('U', 'S'): raise ValueError('invalid kind, must be "U" or "S"') su = '=U' if kind == 'U' else '|S' else: kind = buffer.dtype.kind su = buffer.dtype.byteorder + kind if kind not in ('U', 'S'): raise ValueError('invalid buffer; kind must be "U" or "S"') if shape: if shape[:-1] != buffer.shape[:-1]: raise ValueError('buffer shape does not match that of date array') if buffer.shape[-1] < shape[-1]: raise ValueError('buffer shape is too small for the date array') if kind == 'U': w = 4 # output itemsize null = '' # content of an empty cell sep_ = sep else: w = 1 null = b'\0' sep_ = sep.encode('latin8') # Handle leap seconds and cases of seconds rounding up to the next day digits_ = 0 if digits is None else max(digits, 0) scale = 10 ** digits_ sec = ((sec * scale + 0.5) // 1 + 0.1) / scale # +0.1 to ensure there is no rounding down after division by scale secs_on_day = seconds_on_day(day) crossovers = (sec >= secs_on_day) if shape: day = day.copy() sec = sec.copy() day[crossovers] += 1 sec[crossovers] -= secs_on_day[crossovers] elif crossovers: day += 1 sec -= secs_on_day # DETERMINE WHICH DAYS HAVE LEAP SECONDS! # Determine the field widths by formatting the first value first_index = len(shape) * (0,) day0 = day[first_index] sec0 = sec[first_index] day_formatted = format_day(day0, ymd_order, ydigits=ydigits, dash=dash, proleptic=proleptic) sec_formatted = format_sec(sec0, colon=colon, digits=digits, suffix=suffix) if order[0] == 'T': # if time is first result0 = sec_formatted + sep + day_formatted else: result0 = day_formatted + sep + sec_formatted # For a shapeless case with no buffer, we're basically done if shape == () and buffer is None: if kind == 'U': return result0 else: return result0.encode('latin8') ltime = len(sec_formatted) ldate = len(day_formatted) lsep = len(sep) lstring = ltime + ldate + lsep # Construct the dtype dtype_dict = {} if order[0] == 'T': dtype_dict['time'] = (su + str(ltime), 0) if lsep: dtype_dict['sep'] = (su + str(lsep), ltime * w) dtype_dict['date'] = (su + str(ldate), (ltime + lsep) * w) else: dtype_dict['date'] = (su + str(ldate), 0) if lsep: dtype_dict['sep'] = (su + str(lsep), ldate * w) dtype_dict['time'] = (su + str(ltime), (ldate + lsep) * w) # Create a buffer if necessary; otherwise, check dimensions if buffer is None: buffer = np.empty(shape, dtype='=' + kind + str(lstring)) else: if lstring * w > buffer.dtype.itemsize: raise ValueError('buffer itemsize is too small for the date/time format') if lstring * w < buffer.dtype.itemsize: extra = buffer.dtype.itemsize//w - lstring dtype_dict['extra'] = (su + str(extra), lstring * w) # Fill in the date, time, and separator buffer.fill(null) view = buffer.view(np.dtype(dtype_dict)) if shape: view = view[..., :shape[-1]] if lsep: view['sep'] = sep_ _ = format_day(day, ymd_order, ydigits=ydigits, dash=dash, proleptic=proleptic, buffer=view['date']) _ = format_sec(sec, colon=colon, digits=digits, suffix=suffix, buffer=view['time']) return buffer
########################################################################################## # Date/time formatting using TAI ##########################################################################################
[docs] def format_tai(tai, order='YMDT', *, ydigits=4, dash='-', sep='T', colon=':', digits=None, suffix='', proleptic=False, buffer=None, kind='U'): """Format a date and time given a time in seconds TAI. This function supports scalar or array-like inputs. If array-like inputs are provided, an array of strings or ASCII byte strings is returned. Note that the optional output buffer can be either strings (dtype "U") or bytes (dtype "S"). If the latter, you can define it as a NumPy memmap and write content directly into an ASCII table file. Parameters: tai (int, float, or array): Time value in seconds TAI. order (str): The order of the year, optional month, day, and time fields. Can be any of "YMD", "MDY", "DMY", "YD", or "DY". Add "T" at the beginning or end to indicate whether times come before or after the date. ydigits (int, optional): Number of year digits to include in year, 2 or 4. dash (str): Character(s) to include between fields, if any. Default is "-". Use "" for no separators; any other string can also be used in place of the dashes. sep (str): Character(s) to appear between the date and the time. colon (str): Character(s) to include between fields, if any. Default is ":". Use "" for no separators; any other string can also be used in place of the colons. digits (int, optional): Decimal digits to include second values; use -1 or None to suppress the decimal point; ignored if `sec` values are integers. suffix (str, optional): "Z" to include the Zulu time zone indicator. proleptic (bool, optional): True to interpret all dates according to the modern Gregorian calendar, even those that occurred prior to the transition from the Julian calendar. False to use the Julian calendar for earlier dates. buffer (array[str or bytes]): An optional array of strings or byte strings into which to write the results. Must have sufficient dimensions. This can be either strings (dtype.kind = "U") or bytes (dtype.kind = "S"); if the latter, you can provide a NumPy memmap as input and this function will write content directly into an ASCII table file. kind (str): "U" to return strings, "S" to return bytes. Ignored if `buffer` is provided. Returns: str or array: The formatted date-time value(s). """ (day, sec) = day_sec_from_tai(tai) return format_day_sec(day, sec, order=order, ydigits=ydigits, dash=dash, sep=sep, colon=colon, digits=digits, suffix=suffix, proleptic=proleptic, buffer=buffer, kind=kind)
[docs] def iso_from_tai(tai, ymd=True, digits=None, *, suffix='', proleptic=False, buffer=None, kind='U'): """Date and time in ISO format given seconds TAI. This function supports scalar or array-like inputs. If array-like inputs are provided, an array of strings or ASCII byte strings is returned. Note that the optional output buffer can be either strings (dtype "U") or bytes (dtype "S"). If the latter, you can define it as a NumPy memmap and write content directly into an ASCII table file. Note that this function is a variant of format_day_sec() but with a reduced set of options. Parameters: tai (int, float, or array): Time value in seconds TAI. ymd (bool, optional): True for year-month-day format; False for year plus day-of-year format. digits (int, optional): Decimal digits to include second values; use -1 or None to suppress the decimal point; ignored if `sec` values are integers. suffix (str, optional): "Z" to include the Zulu time zone indicator. proleptic (bool, optional): True to interpret all dates according to the modern Gregorian calendar, even those that occurred prior to the transition from the Julian calendar. False to use the Julian calendar for earlier dates. buffer (array[str or bytes]): An optional array of strings or byte strings into which to write the results. Must have sufficient dimensions. This can be either strings (dtype.kind = "U") or bytes (dtype.kind = "S"); if the latter, you can provide a NumPy memmap as input and this function will write content directly into an ASCII table file. kind (str): "U" to return strings, "S" to return bytes. Ignored if `buffer` is provided. Returns: str or array: The formatted date-time value(s). """ if ymd: return format_tai(tai, order='YMDT', sep='T', digits=digits, suffix=suffix, proleptic=proleptic, buffer=buffer, kind=kind) else: return format_tai(tai, order='YDT', sep='T', digits=digits, suffix=suffix, proleptic=proleptic, buffer=buffer, kind=kind)
##########################################################################################