github.com/qioalice/ekago/v3@v3.3.2-0.20221202205325-5c262d586ee4/ekatime/timestamp_encode.go (about)

     1  // Copyright © 2020. All rights reserved.
     2  // Author: Ilya Stroy.
     3  // Contacts: iyuryevich@pm.me, https://github.com/qioalice
     4  // License: https://opensource.org/licenses/MIT
     5  
     6  package ekatime
     7  
     8  import (
     9  	"errors"
    10  
    11  	"github.com/qioalice/ekago/v3/internal/ekaenc"
    12  )
    13  
    14  //goland:noinspection GoSnakeCaseUsage
    15  var (
    16  	_ERR_NIL_TIMESTAMP_RECEIVER  = errors.New("nil ekatime.Timestamp receiver")
    17  	_ERR_NOT_ISO8601_TIMESTAMP   = errors.New("incorrect ISO8601 timestamp format (must be YYYY-MM-DDThh:mm:ss)")
    18  	_ERR_BAD_TIMESTAMP_SEPARATOR = errors.New("ISO8601 require using 'T' as date time separator")
    19  	_ERR_BAD_JSON_TIMESTAMP_QUO  = errors.New("bad JSON ISO8601 timestamp representation (forgotten quotes?)")
    20  )
    21  
    22  // AppendTo generates a string representation of Timestamp and adds it to the b,
    23  // returning a new slice (if it has grown) or the same if there was enough
    24  // space to store 19 bytes (of string representation).
    25  //
    26  // Uses separator as Date and Time's parts separator:
    27  // "YYYY<separatorDate>MM<separatorDate>DD hh<separatorTime>mm<separatorTime>ss".
    28  //
    29  // Up to 10x faster than fmt.Sprintf().
    30  func (ts Timestamp) AppendTo(b []byte, separatorDate, separatorTime byte) []byte {
    31  
    32  	b = NewDate(dateFromUnix(ts)).AppendTo(b, separatorDate)
    33  	b = append(b, ' ')
    34  	b = NewTime(timeFromUnix(ts)).AppendTo(b, separatorTime)
    35  
    36  	return b
    37  }
    38  
    39  // ParseFrom tries to parse b considering with the following format:
    40  // "YYYY<sep>MM<sep>DD<reqSep>hh<sep>mm".
    41  // - <sep> may be any 1 byte, or not presented at all.
    42  //   But if separator was presented between in "YYYY<sep>MM" it must be also
    43  //   between other parts and vice-versa.
    44  // - <reqSep> required separator: space or 'T' char.
    45  //
    46  // Read more:
    47  // https://en.wikipedia.org/wiki/ISO_8601
    48  //
    49  // If success, returns nil and saves date into the current Timestamp object.
    50  // Year must be <= 4095, instead it will be overwritten by 4095.
    51  //
    52  // Skips leading spaces, ignores all next data after the day has been scanned.
    53  func (ts *Timestamp) ParseFrom(b []byte) error {
    54  
    55  	if ts == nil {
    56  		return _ERR_NIL_TIMESTAMP_RECEIVER
    57  	}
    58  
    59  	var (
    60  		d Date
    61  		t Time
    62  	)
    63  
    64  	// Regardless Date.ParseFrom() has the loop that skips spaces,
    65  	// we need to know we must start parsing Time from.
    66  	// So, skip spaces manually.
    67  
    68  	var i = 0
    69  	for n := len(b); i < n && b[i] <= ' '; i++ {
    70  	}
    71  
    72  	if err := d.ParseFrom(b[i:]); err != nil {
    73  		return err
    74  	}
    75  
    76  	// Date.ParseFrom() finished w/ no errors.
    77  	// So, it was be either 8 chars (YYYYMMDD) or 10 (YYYY-MM-DD).
    78  	//
    79  	// YYYYMMDD
    80  	// YYYY-MM-DD
    81  	//        ^    we need to check this char.
    82  	// If it's a digit, it was 6 char format. Otherwise - 8.
    83  
    84  	i += 7
    85  	if b[i] >= '0' && b[i] <= '9' {
    86  		// Was 8 char format.
    87  		i += 1
    88  	} else {
    89  		// Was 10 char format.
    90  		i += 3
    91  	}
    92  
    93  	// It's OK if time is not presented.
    94  	b = b[i:]
    95  	i = 0
    96  	if len(b) > 0 {
    97  		// ISO8601 requires a separator between date and time. And it must be 'T' char.
    98  		// We allows also a space. Why not?
    99  		if b[i] != ' ' && b[i] != 't' && b[i] != 'T' {
   100  			return _ERR_BAD_TIMESTAMP_SEPARATOR
   101  		}
   102  		i++
   103  		if err := t.ParseFrom(b[i:]); err != nil {
   104  			return err
   105  		}
   106  	}
   107  
   108  	*ts = d.WithTime(t.Split())
   109  	return nil
   110  }
   111  
   112  // String returns the current Timestamp's human-readable string representation
   113  // in the following format: "YYYY/MM/DD hh:mm:ss".
   114  func (ts Timestamp) String() string {
   115  	return string(ts.AppendTo(make([]byte, 0, 19), '/', ':'))
   116  }
   117  
   118  // MarshalJSON encodes the current Time in the following format (quoted)
   119  // "YYYY-MM-DDThh:mm:ss", and returns it. Always returns nil as error.
   120  //
   121  // JSON null supporting:
   122  // - Writes JSON null if current Timestamp receiver == nil.
   123  // - Writes JSON null if current Timestamp == 0 (*).
   124  //
   125  // -----
   126  //
   127  // (*): Yes, it's kinda weird that 01 Jan 1970 00:00:00 is JSON null,
   128  // but let's be honest. When the last time you'll need to marshal EXACTLY
   129  // this date and it's not a null-like value?
   130  // And, of course, you always may use 01 Jan 1970 00:00:01 and it will be marshalled
   131  // correctly.
   132  func (ts *Timestamp) MarshalJSON() ([]byte, error) {
   133  
   134  	if ts == nil || *ts == 0 {
   135  		return ekaenc.NULL_JSON_BYTES_SLICE, nil
   136  	}
   137  
   138  	// Date: 10 chars (YYYY-MM-DD)
   139  	// Clock: 8 chars (hh:mm:ss)
   140  	// Quotes: 2 chars ("")
   141  	// Date clock separator: 1 char (T)
   142  	// Summary: 21 char.
   143  	b := make([]byte, 21)
   144  
   145  	_ = ts.Date().AppendTo(b[1:1:20], '-')
   146  	_ = ts.Time().AppendTo(b[12:12:20], ':')
   147  
   148  	b[0] = '"'
   149  	b[11] = 'T'
   150  	b[20] = '"'
   151  
   152  	return b, nil
   153  }
   154  
   155  // UnmarshalJSON decodes b into the current Timestamp object expecting b contains
   156  // ISO8601 quoted date with time in the one of the following formats:
   157  //   "YYYY-MM-DDThh:mm:ss" (recommended),
   158  //   "YYYYMMDDThh:mm:ss", "YYYY-MM-DDThhmmss", "YYYYMMDDThhmmss"
   159  //
   160  // JSON null supporting:
   161  // - It's ok if there is JSON null and receiver == nil (nothing changes)
   162  // - Zeroes Timestamp if there is JSON null and receiver != nil
   163  //   (yes, sets to 01 Jan 1970 00:00:00).
   164  //
   165  // In other cases JSON parsing error or Date.ParseFrom() error is returned.
   166  func (ts *Timestamp) UnmarshalJSON(b []byte) error {
   167  
   168  	if ekaenc.IsNullJSON(b) {
   169  		if ts != nil {
   170  			*ts = 0
   171  		}
   172  		return nil
   173  	}
   174  
   175  	// Length variants:
   176  	// Date length variants: 10, 8
   177  	// Clock length variants: 4, 5, 6, 8,
   178  	// Quotes: 2,
   179  	// Date time separator: 1.
   180  	//
   181  	// So, summary variants:
   182  	// - 15 (8+4+2+1),
   183  	// - 16 (8+5+2+1),
   184  	// - 17 (8+6+2+1, 10+4+2+1),
   185  	// - 18 (10+5+2+1),
   186  	// - 19 (8+8+2+1, 10+6+2+1),
   187  	// - 21 (10+8+2+1).
   188  
   189  	switch l := len(b); {
   190  
   191  	case !(l >= 15 && l <= 19) && l != 21:
   192  		return _ERR_NOT_ISO8601_TIMESTAMP
   193  
   194  	case b[0] != '"' && b[l-1] != '"':
   195  		return _ERR_BAD_JSON_TIMESTAMP_QUO
   196  
   197  	default:
   198  		return ts.ParseFrom(b[1 : l-1])
   199  	}
   200  }