// Package nsdxml реализует кодек XML НРД для M2M: парсер/сериализатор // windows-1251 и тип NSDDateTime для нестандартного формата отметки // времени, используемого в XSD НРД ("YYYY-MM-DDThh:mm:ss(МСК+N)"). package nsdxml import ( "encoding/xml" "errors" "fmt" "regexp" "strconv" "time" ) // moscowLocation — фиксированная локация UTC+3 для отметок МСК. // Используется именованная локация (без обращения к tzdata) — на стенде // может не быть установленной базы IANA. var moscowLocation = time.FixedZone("МСК", 3*60*60) // nsdDateTimeRegex — паттерн из XSD M2MTypesNSD.xsd // (NSDDateTimeType): дата T время затем (МСК) либо (МСК+N) / (МСК-N). var nsdDateTimeRegex = regexp.MustCompile( `^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T` + `([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])` + `\(МСК(([+-])([0-9]{1,2}))?\)$`, ) // NSDDateTime — отметка времени в формате НРД. Внутреннее // представление — время в зоне Europe/Moscow (UTC+3) плюс смещение в // часах относительно МСК, если оно указано в исходной строке. type NSDDateTime struct { // Time хранится в локации "МСК" (UTC+3). При наличии смещения сам // инстант сдвинут на OffsetHours часов вперёд/назад. Time time.Time // OffsetHours — сдвиг в часах относительно МСК (0 если не задан). OffsetHours int // OffsetSpecified отличает "(МСК)" (false) от "(МСК+0)" (true). // Сохраняется ради round-trip. OffsetSpecified bool } // Now возвращает текущее время в зоне МСК без указания сдвига. func Now() NSDDateTime { return NSDDateTime{Time: time.Now().In(moscowLocation)} } // String возвращает строковое представление в формате НРД // "YYYY-MM-DDThh:mm:ss(МСК[+-N])". func (d NSDDateTime) String() string { mt := d.Time.In(moscowLocation) base := fmt.Sprintf( "%04d-%02d-%02dT%02d:%02d:%02d", mt.Year(), int(mt.Month()), mt.Day(), mt.Hour(), mt.Minute(), mt.Second(), ) switch { case !d.OffsetSpecified: return base + "(МСК)" case d.OffsetHours >= 0: return fmt.Sprintf("%s(МСК+%d)", base, d.OffsetHours) default: return fmt.Sprintf("%s(МСК%d)", base, d.OffsetHours) } } // parseNSDDateTime разбирает строку формата НРД в NSDDateTime. func parseNSDDateTime(s string) (NSDDateTime, error) { m := nsdDateTimeRegex.FindStringSubmatch(s) if m == nil { return NSDDateTime{}, fmt.Errorf("nsdxml: NSDDateTime %q не соответствует формату", s) } year, _ := strconv.Atoi(m[1]) month, _ := strconv.Atoi(m[2]) day, _ := strconv.Atoi(m[3]) hour, _ := strconv.Atoi(m[4]) min, _ := strconv.Atoi(m[5]) sec, _ := strconv.Atoi(m[6]) t := time.Date(year, time.Month(month), day, hour, min, sec, 0, moscowLocation) out := NSDDateTime{Time: t} if m[7] != "" { val, _ := strconv.Atoi(m[9]) if m[8] == "-" { val = -val } out.OffsetHours = val out.OffsetSpecified = true } return out, nil } // MarshalText сериализует NSDDateTime в строку формата НРД. func (d NSDDateTime) MarshalText() ([]byte, error) { return []byte(d.String()), nil } // UnmarshalText разбирает строку формата НРД в NSDDateTime. func (d *NSDDateTime) UnmarshalText(b []byte) error { if len(b) == 0 { return errors.New("nsdxml: пустое значение NSDDateTime") } parsed, err := parseNSDDateTime(string(b)) if err != nil { return err } *d = parsed return nil } // MarshalXML кодирует NSDDateTime в строку формата НРД. func (d NSDDateTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(d.String(), start) } // UnmarshalXML декодирует строку формата НРД в NSDDateTime. func (d *NSDDateTime) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { var s string if err := dec.DecodeElement(&s, &start); err != nil { return err } return d.UnmarshalText([]byte(s)) }