All files / src/lib/formatters time-utils.ts

100% Statements 45/45
94% Branches 47/50
100% Functions 7/7
100% Lines 45/45

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187                                                      309x 3x     306x 306x     306x 306x 306x 306x     306x 18x 288x 1x 287x 1x     286x     306x                                 23x 23x                             12x                             18x 2x     16x 16x 1x       15x 3x       12x 5x       7x                         8x                         7x 7x 1x       6x 2x       4x 3x       1x                           62x 10x   52x 62x 50x 50x   2x    
/**
 * Time and duration utility functions for formatters.
 *
 * Extracted to break the circular import between `human.ts` and `trace.ts`:
 * both modules need these utilities but neither should depend on the other.
 *
 * Also provides generic compact/verbose duration formatters (seconds-based)
 * used by replay commands and any future duration display.
 */
 
import type { TraceSpan } from "../../types/index.js";
import { colorTag } from "./markdown.js";
 
/**
 * Format a date string as a relative time label.
 *
 * - Under 60 minutes: "5m ago"
 * - Under 24 hours: "3h ago"
 * - Under 3 days: "2d ago"
 * - Otherwise: short date like "Jan 18"
 *
 * Returns a muted "—" when the input is undefined.
 *
 * @param dateString - ISO date string or undefined
 * @returns Human-readable relative time string
 */
export function formatRelativeTime(dateString: string | undefined): string {
  if (!dateString) {
    return colorTag("muted", "—");
  }
 
  const date = new Date(dateString);
  const now = Date.now();
  // Clamp to >= 0 so clock skew, scheduled/future timestamps, or bad API data
  // render as "0m ago" instead of a negative duration like "-5m ago".
  const diffMs = Math.max(0, now - date.getTime());
  const diffMins = Math.floor(diffMs / 60_000);
  const diffHours = Math.floor(diffMs / 3_600_000);
  const diffDays = Math.floor(diffMs / 86_400_000);
 
  let text: string;
  if (diffMins < 60) {
    text = `${diffMins}m ago`;
  } else if (diffHours < 24) {
    text = `${diffHours}h ago`;
  } else if (diffDays < 3) {
    text = `${diffDays}d ago`;
  } else {
    // Short date: "Jan 18"
    text = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
  }
 
  return text;
}
 
// ---------------------------------------------------------------------------
// Generic duration formatting (seconds-based)
// ---------------------------------------------------------------------------
 
/**
 * Split a duration in seconds into days, hours, minutes, and seconds.
 * Rounds to the nearest second and clamps to non-negative.
 */
function splitDuration(totalSeconds: number): {
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
} {
  const rounded = Math.max(0, Math.round(totalSeconds));
  return {
    days: Math.floor(rounded / 86_400),
    hours: Math.floor((rounded % 86_400) / 3600),
    minutes: Math.floor((rounded % 3600) / 60),
    seconds: rounded % 60,
  };
}
 
/**
 * Pluralize a value with its singular unit name.
 *
 * @example pluralize(1, "minute") → "1 minute"
 * @example pluralize(3, "hour") → "3 hours"
 */
function pluralize(value: number, singular: string): string {
  return `${value} ${singular}${value === 1 ? "" : "s"}`;
}
 
/**
 * Format a duration (in seconds) as a compact string for table/list output.
 *
 * Shows at most two adjacent units: `2m 5s`, `1h 1m`, `1d 1h`.
 * Returns `"—"` when the input is null or undefined.
 *
 * @param seconds - Duration in seconds, or null/undefined
 * @returns Compact duration string (e.g., `"2m 5s"`, `"1d"`, `"—"`)
 */
export function formatDurationCompact(
  seconds: number | null | undefined
): string {
  if (seconds === null || seconds === undefined) {
    return "—";
  }
 
  const parts = splitDuration(seconds);
  if (parts.days > 0) {
    return parts.hours > 0
      ? `${parts.days}d ${parts.hours}h`
      : `${parts.days}d`;
  }
  if (parts.hours > 0) {
    return parts.minutes > 0
      ? `${parts.hours}h ${parts.minutes}m`
      : `${parts.hours}h`;
  }
  if (parts.minutes > 0) {
    return parts.seconds > 0
      ? `${parts.minutes}m ${parts.seconds}s`
      : `${parts.minutes}m`;
  }
  return `${parts.seconds}s`;
}
 
/**
 * Format a duration (in milliseconds) as a compact string.
 *
 * Converts ms → seconds and delegates to {@link formatDurationCompact}.
 * Useful for activity offsets and other ms-based durations.
 *
 * @param milliseconds - Duration in milliseconds
 * @returns Compact duration string (e.g., `"2m 5s"`, `"1h"`)
 */
export function formatDurationCompactMs(milliseconds: number): string {
  return formatDurationCompact(milliseconds / 1000);
}
 
/**
 * Format a duration (in seconds) as a verbose human-readable string.
 *
 * Uses full unit names with "and" joining the two most significant units:
 * `"2 minutes and 5 seconds"`, `"1 hour and 1 minute"`, `"1 day"`.
 *
 * @param seconds - Duration in seconds
 * @returns Verbose duration string
 */
export function formatDurationVerbose(seconds: number): string {
  const parts = splitDuration(seconds);
  if (parts.days > 0) {
    return parts.hours > 0
      ? `${pluralize(parts.days, "day")} and ${pluralize(parts.hours, "hour")}`
      : pluralize(parts.days, "day");
  }
  if (parts.hours > 0) {
    return parts.minutes > 0
      ? `${pluralize(parts.hours, "hour")} and ${pluralize(parts.minutes, "minute")}`
      : pluralize(parts.hours, "hour");
  }
  if (parts.minutes > 0) {
    return parts.seconds > 0
      ? `${pluralize(parts.minutes, "minute")} and ${pluralize(parts.seconds, "second")}`
      : pluralize(parts.minutes, "minute");
  }
  return pluralize(parts.seconds, "second");
}
 
// ---------------------------------------------------------------------------
// Span duration
// ---------------------------------------------------------------------------
 
/**
 * Compute the duration of a span in milliseconds.
 * Prefers the API-provided `duration` field, falls back to timestamp arithmetic.
 *
 * @returns Duration in milliseconds, or undefined if not computable
 */
export function computeSpanDurationMs(span: TraceSpan): number | undefined {
  if (span.duration !== undefined && Number.isFinite(span.duration)) {
    return span.duration;
  }
  const endTs = span.end_timestamp || span.timestamp;
  if (endTs !== undefined && Number.isFinite(endTs)) {
    const ms = (endTs - span.start_timestamp) * 1000;
    return ms >= 0 ? ms : undefined;
  }
  return;
}