Files
fontvielle 1d6ab86a57 feat(m2m): доменная модель сообщений + парсер windows-1251 + round-trip тесты
- 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>
2026-05-14 00:30:46 +03:00

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))
}