/**
* @fileoverview MetricValue value object
*
* This module defines the MetricValue value object which represents
* a measured metric value with its unit and display formatting.
*/
import { ValueObject } from '../shared/value-object.js';
/**
* Properties for the MetricValue value object
*/
interface MetricValueProps {
value: number;
unit: string;
displayValue: string;
measuredAt: Date;
}
/**
* Value object representing a metric measurement
*
* Encapsulates a metric value with its unit, display formatting,
* and the timestamp when it was measured.
*
* @example
* ```typescript
* const coverage = MetricValue.create(85.5, '%');
* console.log(coverage.value); // 85.5
* console.log(coverage.displayValue); // "85.5%"
*
* const complexity = MetricValue.create(12, 'points', '12/20');
* console.log(complexity.displayValue); // "12/20"
* ```
*/
export class MetricValue extends ValueObject<MetricValueProps> {
// Private constructor enforces factory pattern - instances must be created via static methods
// skipcq: JS-0358 - This constructor is necessary to prevent direct instantiation
private constructor(props: MetricValueProps) {
super(props);
}
/**
* Creates a new MetricValue instance
*
* @param value - The numeric value of the metric
* @param unit - The unit of measurement
* @param displayValue - Optional custom display value (defaults to value + unit)
* @param measuredAt - Optional measurement timestamp (defaults to now)
* @returns A new MetricValue instance
* @throws Error if the value is not a valid number
*/
static create(
value: number,
unit: string,
displayValue?: string,
measuredAt?: Date
): MetricValue {
if (!Number.isFinite(value)) {
throw new Error('Metric value must be a finite number');
}
if (unit.length === 0) {
throw new Error('Unit cannot be empty');
}
const display = displayValue || `${value}${unit}`;
const timestamp = measuredAt || new Date();
return new MetricValue({
value,
unit,
displayValue: display,
measuredAt: timestamp,
});
}
/**
* Creates a percentage metric value
*
* @param value - The percentage value
* @param decimalPlaces - Number of decimal places to display (default: 1)
* @returns A new MetricValue instance for percentages
*/
static createPercentage(value: number, decimalPlaces = 1): MetricValue {
const displayValue = `${value.toFixed(decimalPlaces)}%`;
return MetricValue.create(value, '%', displayValue);
}
/**
* Creates a null/unknown metric value
*
* @param unit - The unit of measurement
* @returns A new MetricValue instance representing an unknown value
*/
static createUnknown(unit: string): MetricValue {
return MetricValue.create(0, unit, 'N/A');
}
/**
* Gets the numeric value
*/
get value(): number {
return this.props.value;
}
/**
* Gets the unit of measurement
*/
get unit(): string {
return this.props.unit;
}
/**
* Gets the display-formatted value
*/
get displayValue(): string {
return this.props.displayValue;
}
/**
* Gets the measurement timestamp
*/
get measuredAt(): Date {
return this.props.measuredAt;
}
/**
* Checks if this is a percentage value
*/
get isPercentage(): boolean {
return this.props.unit === '%';
}
/**
* Checks if this represents an unknown/null value
*/
get isUnknown(): boolean {
return this.props.displayValue === 'N/A';
}
/**
* Calculates the difference between this value and another
*
* @param other - The other metric value to compare
* @returns The numeric difference
* @throws Error if the units don't match
*/
difference(other: MetricValue): number {
if (this.props.unit !== other.props.unit) {
throw new Error('Cannot calculate difference between metrics with different units');
}
return this.props.value - other.props.value;
}
/**
* Calculates the percentage change from another value
*
* @param previous - The previous metric value
* @returns The percentage change
* @throws Error if the units don't match
*/
percentageChange(previous: MetricValue): number {
if (this.props.unit !== previous.props.unit) {
throw new Error('Cannot calculate percentage change between metrics with different units');
}
if (previous.props.value === 0) {
return this.props.value > 0 ? 100 : 0;
}
return ((this.props.value - previous.props.value) / previous.props.value) * 100;
}
/**
* Returns a string representation of the metric value
*/
override toString(): string {
return this.props.displayValue;
}
}