initial commit

This commit is contained in:
2026-04-07 17:41:25 +02:00
commit 1ed9bdfa55
45 changed files with 4712 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
// Package timeutil provides time-related utility functions.
package timeutil
import (
"fmt"
"strings"
"time"
)
// pluralize returns singular or plural form based on count
func pluralize(count int, singular, plural string) string {
if count == 1 {
return fmt.Sprintf("1 %s", singular)
}
return fmt.Sprintf("%d %s", count, plural)
}
// formatTimeComponents formats non-zero time components into a string
func formatTimeComponents(parts []string) string {
return strings.Join(parts, " ")
}
// formatWeeksAndDays formats weeks and remaining days
func formatWeeksAndDays(days int) []string {
var parts []string
weeks := days / 7
remainingDays := days % 7
if weeks > 0 {
parts = append(parts, pluralize(weeks, "week", "weeks"))
}
if remainingDays > 0 {
parts = append(parts, pluralize(remainingDays, "day", "days"))
}
return parts
}
// formatHoursMinutes formats hours and minutes
func formatHoursMinutes(hours, minutes int) string {
switch {
case hours > 0 && minutes > 0:
return fmt.Sprintf("%d hours %d minutes", hours, minutes)
case hours > 0:
return fmt.Sprintf("%d hours", hours)
default:
return fmt.Sprintf("%d minutes", minutes)
}
}
// formatDaysHoursMinutes formats days, hours, and minutes into a string
func formatDaysHoursMinutes(days, hours, minutes int) string {
// Simple days format (less than a week)
if days < 7 {
return formatSimpleDays(days, hours, minutes)
}
// Weeks format
parts := formatWeeksAndDays(days)
if hours > 0 || minutes > 0 {
parts = append(parts, formatHoursMinutes(hours, minutes))
}
return formatTimeComponents(parts)
}
// formatSimpleDays formats days less than a week with hours and minutes
func formatSimpleDays(days, hours, minutes int) string {
if hours == 0 && minutes == 0 {
return pluralize(days, "day", "days")
}
if minutes == 0 {
return fmt.Sprintf("%d days %d hours", days, hours)
}
return fmt.Sprintf("%d days %d hours %d minutes", days, hours, minutes)
}
// HumanizeDuration converts a duration to a human-readable format with high precision.
// It shows progressively less detail for longer durations:
// - Less than a minute: "less than a minute"
// - Less than an hour: "X minutes"
// - Less than a day: "X hours Y minutes"
// - Less than a week: "X days Y hours Z minutes"
// - Less than a month: "X weeks Y days Z hours W minutes"
// - Longer periods: months and years with appropriate detail
func HumanizeDuration(d time.Duration) string {
if d < 0 {
return "expired"
}
if d < time.Minute {
return "less than a minute"
}
if d < time.Hour {
return pluralize(int(d.Minutes()), "minute", "minutes")
}
if d < 24*time.Hour {
return formatHoursDuration(d)
}
days := int(d.Hours() / 24)
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
if days < 30 {
return formatDaysHoursMinutes(days, hours, minutes)
}
if days < 365 {
return formatMonthsDuration(days)
}
return formatYearsDuration(days)
}
// formatHoursDuration formats durations less than a day
func formatHoursDuration(d time.Duration) string {
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
if minutes == 0 {
return pluralize(hours, "hour", "hours")
}
return fmt.Sprintf("%d hours %d minutes", hours, minutes)
}
// formatMonthsDuration formats durations between 30 and 365 days
func formatMonthsDuration(days int) string {
months := days / 30
remainingDays := days % 30
if remainingDays == 0 {
return pluralize(months, "month", "months")
}
return fmt.Sprintf("%d months %d days", months, remainingDays)
}
// formatYearsDuration formats durations of 365 days or more
func formatYearsDuration(days int) string {
years := days / 365
remainingDays := days % 365
if remainingDays == 0 {
return pluralize(years, "year", "years")
}
if remainingDays < 30 {
return fmt.Sprintf("%d years %d days", years, remainingDays)
}
months := remainingDays / 30
return fmt.Sprintf("%d years %d months", years, months)
}

View File

@@ -0,0 +1,81 @@
package timeutil
import (
"fmt"
"strconv"
"strings"
"time"
)
// parseDaysDuration handles duration strings like "7d", "30d", "365d"
func parseDaysDuration(durationStr string) (time.Duration, error) {
if !strings.HasSuffix(durationStr, "d") {
return 0, fmt.Errorf("not a day duration format")
}
daysStr := strings.TrimSuffix(durationStr, "d")
days, err := strconv.Atoi(daysStr)
if err != nil {
return 0, fmt.Errorf("invalid day value: %s", daysStr)
}
return time.Duration(days) * 24 * time.Hour, nil
}
// ParseDuration parses a duration string that can be either Go duration or "Xd" format
func ParseDuration(durationStr string) (time.Duration, error) {
// Try days format first
if duration, err := parseDaysDuration(durationStr); err == nil {
return duration, nil
}
// Fall back to standard Go duration
return time.ParseDuration(durationStr)
}
// ParseToMinutes parses a duration string and converts it to minutes for the ProtonVPN API.
// Accepts formats like "7d", "30d", "365d", "30m", "24h", "1h30m".
// Returns the duration in "XXX min" format as expected by the API.
func ParseToMinutes(durationStr string) (string, error) {
duration, err := ParseDuration(durationStr)
if err != nil {
return "", fmt.Errorf("invalid duration format: %s", durationStr)
}
// Convert to minutes
minutes := int(duration.Minutes())
if minutes < 1 {
return "", fmt.Errorf("duration must be at least 1 minute")
}
// Max 365 days = 525600 minutes
maxMinutes := 365 * 24 * 60
if minutes > maxMinutes {
return "", fmt.Errorf("duration cannot exceed 365 days")
}
return fmt.Sprintf("%d min", minutes), nil
}
// ParseSessionDuration parses a session duration string for local caching.
// Accepts formats like "7d", "30d", or standard Go duration strings.
// Returns 0 to indicate "use API default".
func ParseSessionDuration(durationStr string) (time.Duration, error) {
// Handle "0" as use API default
if durationStr == "0" {
return 0, nil
}
duration, err := ParseDuration(durationStr)
if err != nil {
return 0, fmt.Errorf("invalid session duration format: %s", durationStr)
}
// Validate max duration (30 days)
maxDuration := 30 * 24 * time.Hour
if duration > maxDuration {
return 0, fmt.Errorf("session duration cannot exceed 30 days (got: %s)", duration)
}
return duration, nil
}