package ted import ( "fmt" "strconv" "strings" "time" ) // ParseIssueDateTimeToUnix combines cbc:IssueDate and cbc:IssueTime (with timezone offsets) into one Unix timestamp. func ParseIssueDateTimeToUnix(issueDate, issueTime string) int64 { issueDate = strings.TrimSpace(issueDate) issueTime = strings.TrimSpace(issueTime) if issueDate == "" { return 0 } if issueTime == "" { return ParseDateToUnix(issueDate) } dateCore := issueDate if len(issueDate) >= 10 && issueDate[4] == '-' && issueDate[7] == '-' { dateCore = issueDate[:10] } combined := dateCore + "T" + issueTime if t, err := ParseDate(combined); err == nil { return t.Unix() } return ParseDateToUnix(issueDate) } // TransmissionDispatchUnix reads BT-803 (eSender) transmission date/time from extension content (root or eForms extension). func TransmissionDispatchUnix(ext *UBLExtensions) int64 { if ext == nil || ext.UBLExtension == nil || ext.UBLExtension.ExtensionContent == nil { return 0 } ec := ext.UBLExtension.ExtensionContent if u := ParseIssueDateTimeToUnix(ec.TransmissionDate, ec.TransmissionTime); u != 0 { return u } if ec.EformsExtension != nil { if u := ParseIssueDateTimeToUnix(ec.EformsExtension.TransmissionDate, ec.EformsExtension.TransmissionTime); u != 0 { return u } } return 0 } // ParseEuropeanAmount normalizes TED amount strings (spaces, comma decimal) for strconv.ParseFloat. func ParseEuropeanAmount(raw string) (float64, bool) { s := strings.TrimSpace(raw) s = strings.ReplaceAll(s, "\u00a0", "") s = strings.ReplaceAll(s, " ", "") s = strings.ReplaceAll(s, ",", ".") if s == "" { return 0, false } v, err := strconv.ParseFloat(s, 64) return v, err == nil } // parseDate parses various date formats used in TED XML func ParseDate(dateStr string) (time.Time, error) { if dateStr == "" { return time.Time{}, fmt.Errorf("empty date string") } // Common TED date formats formats := []string{ "2006-01-02+07:00", "2006-01-02-07:00", "2006-01-02T15:04:05+07:00", "2006-01-02T15:04:05-07:00", "2006-01-02T15:04:05Z", "2006-01-02Z", "2006-01-02", time.RFC3339, time.RFC3339Nano, } for _, format := range formats { if parsedTime, err := time.Parse(format, dateStr); err == nil { return parsedTime, nil } } return time.Time{}, fmt.Errorf("unable to parse date: %s", dateStr) } // parseDateToUnixMilli converts string date to Unix milliseconds (int64) func ParseDateToUnix(dateStr string) int64 { if dateStr == "" { return 0 } if parsedTime, err := ParseDate(dateStr); err == nil { return parsedTime.Unix() } return 0 } // parseTimeToUnix converts string time to Unix milliseconds (int64) func ParseTimeToUnix(timeStr string) int64 { if timeStr == "" { return 0 } // If time string is just time without date, use today's date if !strings.Contains(timeStr, "T") && !strings.Contains(timeStr, " ") { // Assume it's just a time like "15:04:05" today := time.Now().Format("2006-01-02") timeStr = fmt.Sprintf("%sT%s", today, timeStr) } if parsedTime, err := ParseDate(timeStr); err == nil { return parsedTime.Unix() } return 0 } func CalculateSubmissionDeadline(publicationDate time.Time) time.Time { deadline := publicationDate workingHoursAdded := 0 for workingHoursAdded < 48 { deadline = deadline.Add(1 * time.Hour) if deadline.Weekday() != time.Saturday && deadline.Weekday() != time.Sunday { workingHoursAdded++ } } return deadline } func CalculateApplicationDeadline(tenderDeadline time.Time) time.Time { applicationDeadline := tenderDeadline workingDaysSubtracted := 0 for workingDaysSubtracted < 7 { applicationDeadline = applicationDeadline.AddDate(0, 0, -1) if applicationDeadline.Weekday() != time.Saturday && applicationDeadline.Weekday() != time.Sunday { workingDaysSubtracted++ } } return applicationDeadline } // generateTenderURL generates the TED tender URL from NoticePublicationID func GenerateTenderURL(noticePublicationID string) string { if noticePublicationID == "" { return "" } // Remove leading zeros and format for URL // Example: 00522942-2025 -> https://ted.europa.eu/en/notice/-/detail/522942-2025 parts := strings.Split(noticePublicationID, "-") if len(parts) != 2 { return "" } // Convert to int and back to string to remove leading zeros if num, err := strconv.Atoi(parts[0]); err == nil { cleanID := fmt.Sprintf("%d-%s", num, parts[1]) return fmt.Sprintf("https://ted.europa.eu/en/notice/-/detail/%s", cleanID) } return "" }