github.com/qioalice/ekago/v3@v3.3.2-0.20221202205325-5c262d586ee4/ekatime/date_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  	"strconv"
    11  
    12  	"github.com/qioalice/ekago/v3/internal/ekaenc"
    13  )
    14  
    15  //goland:noinspection GoSnakeCaseUsage
    16  var (
    17  	_ERR_NIL_DATE_RECEIVER = errors.New("nil ekatime.Date receiver")
    18  	_ERR_NOT_ISO8601_DATE  = errors.New("incorrect ISO8601 date format (must be YYYYMMDD or YYYY-MM-DD)")
    19  	_ERR_BAD_YEAR          = errors.New("year must be in the range [0..4095]")
    20  	_ERR_BAD_MONTH         = errors.New("month must be in the range [1..12]")
    21  	_ERR_BAD_DAY           = errors.New("day must be in the range [1..31]")
    22  	_ERR_BAD_CORRESP_DATE  = errors.New("date must represent valid date (check year, month, day corresponding)")
    23  	_ERR_BAD_JSON_DATE_QUO = errors.New("bad JSON ISO8601 date representation (forgotten quotes?)")
    24  )
    25  
    26  // AppendTo generates a string representation of Date and adds it to the b,
    27  // returning a new slice (if it has grown) or the same if there was enough
    28  // space to store 10 bytes (of string representation).
    29  //
    30  // Uses separator as Date's parts separator:
    31  // "YYYY<separator>MM<separator>DD".
    32  //
    33  // Up to 6x faster than fmt.Sprintf().
    34  func (dd Date) AppendTo(b []byte, separator byte) []byte {
    35  
    36  	f := func(b []byte) {
    37  		y, m, d := normalizeDate(dd.Split())
    38  
    39  		if y >= _YEAR_AS_NUM_STR_MIN && y <= _YEAR_AS_NUM_STR_MAX {
    40  			copy(b[:4], _YEAR_AS_NUM_STR[y-_YEAR_AS_NUM_STR_MIN])
    41  		} else {
    42  			// normalizeDate never return y > 4095, so it's safe
    43  			_ = append(b[:0], strconv.Itoa(int(y))...)
    44  		}
    45  
    46  		copy(b[5:7], _DAY_AS_NUM_STR[m-MONTH_JANUARY]) // use day's array, it's ok
    47  		copy(b[8:10], _DAY_AS_NUM_STR[d-1])
    48  
    49  		b[4] = separator
    50  		b[7] = separator
    51  	}
    52  
    53  	if c, l := cap(b), len(b); c-l >= 10 {
    54  		b = b[:l+10]
    55  		f(b[l:])
    56  		return b
    57  	} else {
    58  		// One more allocation
    59  		b2 := make([]byte, 10)
    60  		f(b2)
    61  		b = append(b, b2...)
    62  		return b
    63  	}
    64  }
    65  
    66  // ParseFrom tries to parse b considering with the following format:
    67  // "YYYY<separator>MM<separator>DD", <separator> may be any 1 byte,
    68  // or not presented at all. But if separator was presented between in "YYYY<sep>MM"
    69  // it must be also between other parts and vice-versa.
    70  //
    71  // Read more:
    72  // https://en.wikipedia.org/wiki/ISO_8601
    73  //
    74  // If success, returns nil and saves date into the current Date object.
    75  // Year must be <= 4095, instead it will be overwritten by 4095.
    76  //
    77  // Skips leading spaces, ignores all next data after the day has been scanned.
    78  func (dd *Date) ParseFrom(b []byte) error {
    79  
    80  	if dd == nil {
    81  		return _ERR_NIL_DATE_RECEIVER
    82  	}
    83  
    84  	var i = 0
    85  	for n := len(b); i < n && b[i] <= ' '; i++ {
    86  	}
    87  
    88  	// Minimum required len: 8 (YYYYMMDD - w/o separators).
    89  	if len(b[i:]) < 8 {
    90  		return _ERR_NOT_ISO8601_DATE
    91  	}
    92  
    93  	x1, valid1 := batoi(b[i], b[i+1])
    94  	x2, valid2 := batoi(b[i+2], b[i+3])
    95  
    96  	x1 *= 100
    97  	x1 += x2
    98  
    99  	if !(valid1 && valid2) || x1 < 0 || x2 < 0 || x1 > 4095 {
   100  		return _ERR_BAD_YEAR
   101  	}
   102  
   103  	i += 4
   104  	y := Year(x1)
   105  	wasSeparator := false
   106  
   107  	// Skip separator
   108  	if !(b[i] >= '0' && b[i] <= '9') {
   109  		i++
   110  		wasSeparator = true
   111  	}
   112  
   113  	x1, valid1 = batoi(b[i], b[i+1])
   114  	m := Month(x1)
   115  	if !valid1 || m < MONTH_JANUARY || m > MONTH_DECEMBER {
   116  		return _ERR_BAD_MONTH
   117  	}
   118  
   119  	i += 2
   120  	if wasSeparator {
   121  		i++
   122  	}
   123  
   124  	if len(b[i:]) < 2 {
   125  		// Looks like user has incorrect data like:
   126  		// "YYYY-MM-D", "YYYY-MM-"
   127  		return _ERR_NOT_ISO8601_DATE
   128  	}
   129  
   130  	x1, valid1 = batoi(b[i], b[i+1])
   131  	if x1 < 1 || x1 > 31 {
   132  		return _ERR_BAD_DAY
   133  	}
   134  
   135  	if !IsValidDate(y, m, Day(x1)) {
   136  		return _ERR_BAD_CORRESP_DATE
   137  	}
   138  
   139  	*dd = NewDate(y, m, Day(x1))
   140  	return nil
   141  }
   142  
   143  // String returns the current Date's string representation in the following format:
   144  // "YYYY/MM/DD".
   145  func (dd Date) String() string {
   146  	return string(dd.AppendTo(make([]byte, 0, 10), '/'))
   147  }
   148  
   149  // MarshalJSON encodes the current Date in the following format (quoted)
   150  // "YYYY-MM-DD", and returns it. Always returns nil as error.
   151  //
   152  // JSON null supporting:
   153  // - Writes JSON null if current Date receiver is nil
   154  // - Writes JSON null if current Date == 0.
   155  func (dd *Date) MarshalJSON() ([]byte, error) {
   156  
   157  	if dd == nil || dd.ToCmp() == 0 {
   158  		return ekaenc.NULL_JSON_BYTES_SLICE, nil
   159  	}
   160  
   161  	b := make([]byte, 12)
   162  	_ = dd.AppendTo(b[1:1:12], '-')
   163  
   164  	b[0] = '"'
   165  	b[11] = '"'
   166  
   167  	return b, nil
   168  }
   169  
   170  // UnmarshalJSON decodes b into the current Date object expecting b contains
   171  // ISO8601 quoted date (only date, not date w/ time) in the one of the following
   172  // formats: "YYYYMMDD", "YYYY-MM-DD".
   173  //
   174  // JSON null supporting:
   175  // - It's ok if there is JSON null and receiver == nil (nothing changes)
   176  // - Zeroes Date if there is JSON null and receiver != nil.
   177  //
   178  // In other cases JSON parsing error or Date.ParseFrom() error is returned.
   179  func (dd *Date) UnmarshalJSON(b []byte) error {
   180  
   181  	if ekaenc.IsNullJSON(b) {
   182  		if dd != nil {
   183  			*dd = 0
   184  		}
   185  		return nil
   186  	}
   187  
   188  	switch l := len(b); {
   189  
   190  	case l != 10 && l != 12:
   191  		// The length must be 10 (8+quotes) or 12 (8+separators+quotes).
   192  		return _ERR_NOT_ISO8601_DATE
   193  
   194  	case b[0] != '"' || b[l-1] != '"':
   195  		// Forgotten quotes? Incorrect JSON?
   196  		return _ERR_BAD_JSON_DATE_QUO
   197  
   198  	default:
   199  		return dd.ParseFrom(b[1 : l-1])
   200  	}
   201  }