Skip to content
97 changes: 97 additions & 0 deletions src/main/java/com/thealgorithms/conversions/TimeConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.thealgorithms.conversions;

import java.util.Locale;
import java.util.Map;

/**
* A utility class to convert between different units of time.
*
* <p>This class supports conversions between the following units:
* <ul>
* <li>seconds</li>
* <li>minutes</li>
* <li>hours</li>
* <li>days</li>
* <li>weeks</li>
* <li>months (approximated as 30.44 days)</li>
* <li>years (approximated as 365.25 days)</li>
* </ul>
*
* <p>The conversion is based on predefined constants in seconds.
* Results are rounded to three decimal places for consistency.
*
* <p>This class is final and cannot be instantiated.
*
* @see <a href="https://en.wikipedia.org/wiki/Unit_of_time">Wikipedia: Unit of time</a>
*/
public final class TimeConverter {

private TimeConverter() {
// Prevent instantiation
}

/**
* Supported time units with their equivalent in seconds.
*/
private enum TimeUnit {
SECONDS(1.0),
MINUTES(60.0),
HOURS(3600.0),
DAYS(86400.0),
WEEKS(604800.0),
MONTHS(2629800.0), // 30.44 days
YEARS(31557600.0); // 365.25 days

private final double seconds;

TimeUnit(double seconds) {
this.seconds = seconds;
}

public double toSeconds(double value) {
return value * seconds;
}

public double fromSeconds(double secondsValue) {
return secondsValue / seconds;
}
}

private static final Map<String, TimeUnit> UNIT_LOOKUP
= Map.ofEntries(Map.entry("seconds", TimeUnit.SECONDS), Map.entry("minutes", TimeUnit.MINUTES), Map.entry("hours", TimeUnit.HOURS), Map.entry("days", TimeUnit.DAYS), Map.entry("weeks", TimeUnit.WEEKS), Map.entry("months", TimeUnit.MONTHS), Map.entry("years", TimeUnit.YEARS));

/**
* Converts a time value from one unit to another.
*
* @param timeValue the numeric value of time to convert; must be non-negative
* @param unitFrom the unit of the input value (e.g., "minutes", "hours")
* @param unitTo the unit to convert into (e.g., "seconds", "days")
* @return the converted value in the target unit, rounded to three decimals
* @throws IllegalArgumentException if {@code timeValue} is negative
* @throws IllegalArgumentException if either {@code unitFrom} or {@code unitTo} is not supported
*/
public static double convertTime(double timeValue, String unitFrom, String unitTo) {
if (timeValue < 0) {
throw new IllegalArgumentException("timeValue must be a non-negative number.");
}

TimeUnit from = resolveUnit(unitFrom);
TimeUnit to = resolveUnit(unitTo);

double secondsValue = from.toSeconds(timeValue);
double converted = to.fromSeconds(secondsValue);

return Math.round(converted * 1000.0) / 1000.0;
}

private static TimeUnit resolveUnit(String unit) {
if (unit == null) {
throw new IllegalArgumentException("Unit cannot be null.");
}
TimeUnit resolved = UNIT_LOOKUP.get(unit.toLowerCase(Locale.ROOT));
if (resolved == null) {
throw new IllegalArgumentException("Invalid unit '" + unit + "'. Supported units are: " + UNIT_LOOKUP.keySet());
}
return resolved;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.thealgorithms.conversions;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;

class TimeConverterTest {

@ParameterizedTest(name = "{0} {1} -> {2} {3}")
@CsvSource({"60, seconds, minutes, 1", "120, seconds, minutes, 2", "2, minutes, seconds, 120", "2, hours, minutes, 120", "1, days, hours, 24", "1, weeks, days, 7", "1, months, days, 30.438", "1, years, days, 365.25", "3600, seconds, hours, 1", "86400, seconds, days, 1",
"604800, seconds, weeks, 1", "31557600, seconds, years, 1"})
void
testValidConversions(double value, String from, String to, double expected) {
assertEquals(expected, TimeConverter.convertTime(value, from, to));
}

@Test
@DisplayName("Zero conversion returns zero")
void testZeroValue() {
assertEquals(0.0, TimeConverter.convertTime(0, "seconds", "hours"));
}

@Test
@DisplayName("Same-unit conversion returns original value")
void testSameUnitConversion() {
assertEquals(123.456, TimeConverter.convertTime(123.456, "minutes", "minutes"));
}

@Test
@DisplayName("Negative value throws exception")
void testNegativeValue() {
assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(-5, "seconds", "minutes"));
}

@ParameterizedTest
@CsvSource({"lightyears, seconds", "minutes, centuries"})
void testInvalidUnits(String from, String to) {
assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(10, from, to));
}

@Test
@DisplayName("Null unit throws exception")
void testNullUnit() {
assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(10, null, "seconds"));

assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(10, "minutes", null));

assertThrows(IllegalArgumentException.class, () -> TimeConverter.convertTime(10, null, null));
}

static Stream<org.junit.jupiter.params.provider.Arguments> roundTripCases() {
return Stream.of(org.junit.jupiter.params.provider.Arguments.of(1.0, "hours", "minutes"), org.junit.jupiter.params.provider.Arguments.of(2.5, "days", "hours"), org.junit.jupiter.params.provider.Arguments.of(1000, "seconds", "minutes"));
}

@ParameterizedTest
@MethodSource("roundTripCases")
@DisplayName("Round-trip conversion returns original value")
void testRoundTripConversion(double value, String from, String to) {
double converted = TimeConverter.convertTime(value, from, to);
double roundTrip = TimeConverter.convertTime(converted, to, from);
assertEquals(Math.round(value * 1000.0) / 1000.0, Math.round(roundTrip * 1000.0) / 1000.0, 0.05);
}
}