All files / src/utils utils.ts

89.13% Statements 41/46
77.96% Branches 46/59
80% Functions 4/5
90% Lines 36/40

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                                            9x     90x 90x 90x   90x 90x 90x                 1833x 1833x   1833x 1833x   1833x                                     12955x   12954x 119x 90x 90x       29x 17x 17x 17x 17x 17x 17x           12x 3x 3x 3x 3x   3x 3x   3x 2x 1x       9x         9128x    
import { canonicalAttr, formatLegendName } from '@ska-octopus-widget-sdk/widget-sdk';
 
// Shared utilities are now in the SDK.  Re-export them so all existing
// internal imports (canonicalAttr, parseMargins, formatLegendName, etc.)
// continue to resolve without touching every call-site.
export {
  canonicalAttr,
  parseMargins,
  formatLegendName,
  extractStationAndNumber
} from '@ska-octopus-widget-sdk/widget-sdk';
 
// ── Timeline-specific utilities ───────────────────────────────────────────
 
export type MinMaxLike =
  | number
  | string
  | { min?: number; max?: number }
  | [number, number]
  | null
  | undefined;
 
const HAS_TEMPLATE_PLACEHOLDER = /\{[^}]+\}/i;
 
function labelAttributePath(attr: string, attrLabel: string): string {
  const key = canonicalAttr(attr);
  const label = String(attrLabel ?? '').trim();
  Iif (!key || !label) return key;
 
  const parts = key.split('/');
  parts[parts.length - 1] = label;
  return parts.join('/');
}
 
export function formatLegendLabel(
  attr: string,
  format: string,
  attrLabel = '',
  hasExplicitFormat = false
): string {
  const label = String(attrLabel ?? '').trim();
  const fmt = String(format ?? 'lastname').trim() || 'lastname';
  const effectiveFmt =
    !hasExplicitFormat && label && !HAS_TEMPLATE_PLACEHOLDER.test(fmt) ? 'label' : fmt;
  const sourceAttr = label ? labelAttributePath(attr, label) : attr;
 
  return formatLegendName(sourceAttr, effectiveFmt, label).trim();
}
 
/* Coerce to number; non-numeric → NaN */
export function toNum(v: any): number {
  const n = typeof v === 'number' ? v : Number(v);
  return Number.isFinite(n) ? n : NaN;
}
 
/**
 * Try to coerce a value coming from HDB++ into:
 *  - y: a scalar (mid = (min+max)/2 when both exist)
 *  - min/max: when available
 */
export function coerceMinMax(value: MinMaxLike): {
  y: number | null;
  min?: number;
  max?: number;
} {
  if (value === null || value === undefined) return { y: null };
 
  if (typeof value === 'number') return { y: Number.isFinite(value) ? value : null };
  if (typeof value === 'string') {
    const num = Number(value);
    return { y: Number.isFinite(num) ? num : null };
  }
 
  // tuple [min, max]
  if (Array.isArray(value) && value.length >= 2) {
    const a = Number(value[0]);
    const b = Number(value[1]);
    Eif (Number.isFinite(a) && Number.isFinite(b)) {
      const min = Math.min(a, b);
      const max = Math.max(a, b);
      return { y: (min + max) / 2, min, max };
    }
    return { y: null };
  }
 
  // object {min, max}
  if (typeof value === 'object') {
    const minRaw = (value as any).min;
    const maxRaw = (value as any).max;
    const min = Number(minRaw);
    const max = Number(maxRaw);
 
    const hasMin = Number.isFinite(min);
    const hasMax = Number.isFinite(max);
 
    if (hasMin && hasMax) return { y: (min + max) / 2, min, max };
    if (hasMin && !hasMax) return { y: min, min };
    Eif (!hasMin && hasMax) return { y: max, max };
    return { y: null };
  }
 
  return { y: null };
}
 
/** Convenience: scalar numeric from arbitrary HDB++ value (or null) */
export function numericValue(value: MinMaxLike): number | null {
  return coerceMinMax(value).y;
}