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>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// 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))
|
||||
}
|
||||
Reference in New Issue
Block a user