1d6ab86a57
- internal/m2m/types.go: enum'ы и simple-типы из XSD НРД (M2MSchemas_260408) - internal/m2m/validators.go: pattern-валидаторы ReferenceID/ISIN/INN/UUID/SecurityCode/IdentityDocSerial/AccountID + перечисления - internal/m2m/messages.go: структуры 6 типов сообщений M2M, choice-типы через указатели, IsM2M=true автоматически в MarshalXML - internal/nsdxml/datetime.go: тип NSDDateTime (формат "YYYY-MM-DDThh:mm:ss(МСК+N)") - internal/nsdxml/codec.go: Marshal/Unmarshal XML в windows-1251 (собственный кодек CP1251, без внешних зависимостей) - internal/m2m/messages_test.go: round-trip тесты на 6 примерах + 2 эталонах из DOC/ Покрытие: m2m 73.9%, nsdxml 92.5%. make ci зелёный. Отклонение от спеки: вместо golang.org/x/text/encoding/charmap собственная таблица CP1251 на ~60 строк, потому что прокси zetit блокирует proxy.golang.org, goproxy.cn и redirect-хосты Go-модулей. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
4.4 KiB
Go
124 lines
4.4 KiB
Go
// 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))
|
|
}
|