Refactoring FormatWatchTime: A Centralized Utility Guide
In software development, the DRY (Don't Repeat Yourself) principle is paramount. It advocates for minimizing code duplication to enhance maintainability, readability, and efficiency. This article delves into a practical application of this principle by centralizing a duplicate formatWatchTime utility across a Plex-related project. We will explore the problem of duplicated code, the proposed solution, and the steps involved in refactoring.
Understanding the Issue: Code Duplication
Code duplication is a common ailment in software projects, especially those that evolve rapidly or involve multiple developers. It occurs when the same or very similar code blocks are repeated in different parts of the codebase. While seemingly innocuous at first, code duplication can lead to a myriad of problems:
- Increased Maintenance Burden: When a bug is discovered or a feature needs to be updated, developers must make the same changes in multiple places, increasing the risk of overlooking instances and introducing inconsistencies.
- Reduced Readability: Duplicate code makes it harder to understand the overall structure and logic of the application. It clutters the codebase and makes it difficult to follow the flow of execution.
- Higher Risk of Errors: Copy-pasting code is error-prone. Small variations in duplicated code can lead to subtle bugs that are hard to track down.
- Bloated Codebase: Duplication increases the size of the codebase, which can impact performance and increase build times.
In the context of our Plex project, the formatWatchTime function, responsible for converting watch time in minutes into a human-readable format (e.g., "2d 5h 30m" or "45m"), was found to be duplicated across multiple component files. This redundancy not only violated the DRY principle but also posed a maintenance challenge. Identifying and consolidating these instances became crucial for the long-term health of the project.
Identifying Duplicate Locations
The initial step in addressing code duplication is to identify all instances of the duplicated code. In our case, the formatWatchTime function was found in the following locations:
- components/admin/prompts/prompt-template-editor.tsx: This file contained two nearly identical implementations, differing only in naming conventions. This immediately highlighted the inconsistency that can arise from duplicated code.
- components/wrapped/wrapped-sections/utils.ts: Another implementation of the function resided here, indicating a lack of a centralized utility module.
- components/wrapped/wrapped-sections/top-movies-section.tsx: An inline implementation was present within this component, suggesting a localized solution to a common problem.
- components/wrapped/wrapped-sections/total-watch-time-section.tsx: Similar to the previous point, an inline implementation was found here.
- components/wrapped/wrapped-sections/shows-breakdown-section.tsx: Another inline implementation, further emphasizing the need for centralization.
- components/wrapped/wrapped-sections/movies-breakdown-section.tsx: Yet another inline implementation, solidifying the case for a unified approach.
The presence of the formatWatchTime function in these multiple locations underscored the severity of the duplication issue. It was clear that a centralized solution was necessary to streamline the codebase and improve maintainability.
The Proposed Solution: Centralization and a Utility Function
To address the code duplication issue, the proposed solution involves creating a single, centralized utility function for formatting watch time. This approach aligns with the DRY principle by ensuring that the formatting logic is defined in one place and reused across the application. The chosen location for this utility is lib/utils/time-formatting.ts, a logical place for time-related formatting functions.
Implementing the formatWatchTime Function
The core of the solution is the formatWatchTime function itself. This function takes the duration in minutes as input and returns a human-readable string representation. Let's examine the implementation:
/**
* Formats duration in minutes to human-readable string
* @param minutes - Duration in minutes
* @returns Formatted string like "2d 5h 30m" or "45m"
*/
export function formatWatchTime(minutes: number): string {
const days = Math.floor(minutes / (24 * 60))
const hours = Math.floor((minutes % (24 * 60)) / 60)
const mins = Math.floor(minutes % 60)
const parts: string[] = []
if (days > 0) parts.push(`${days}d`)
if (hours > 0) parts.push(`${hours}h`)
if (mins > 0 || parts.length === 0) parts.push(`${mins}m`)
return parts.join(' ')
}
This function performs the following steps:
- Calculate Days, Hours, and Minutes: It first calculates the number of days, hours, and minutes from the input
minutesusing mathematical operations. - Build Parts Array: It then creates an array called
partsto store the formatted time components (days, hours, minutes). - Conditional Push: It conditionally pushes the days, hours, and minutes to the
partsarray if their values are greater than 0. The minutes are always pushed if no other parts are present (to handle cases like "45m"). - Join and Return: Finally, it joins the elements of the
partsarray with a space as a separator and returns the resulting formatted string.
Addressing Additional Duplication: formatBytes
While focusing on formatWatchTime, the analysis also revealed duplication of another utility function: formatBytes. This function is responsible for converting bytes into a human-readable format (e.g., "1.5 GB" or "256 MB"). The original issue identified an instance of formatBytes in deletion-history-client.tsx. Therefore, the proposed solution extends to consolidating formatBytes as well.
/**
* Formats bytes to human-readable string
* @param bytes - Size in bytes
* @returns Formatted string like "1.5 GB" or "256 MB"
*/
export function formatBytes(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let unitIndex = 0
let value = bytes
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024
unitIndex++
}
return `${value.toFixed(1)} ${units[unitIndex]}`
}
This function operates as follows:
- Define Units: It defines an array
unitscontaining the abbreviations for byte units (B, KB, MB, GB, TB). - Initialize Variables: It initializes
unitIndexto 0 andvalueto the inputbytes. - Iterative Conversion: It enters a
whileloop that continues as long as thevalueis greater than or equal to 1024 and theunitIndexis within the bounds of theunitsarray. Inside the loop, it divides thevalueby 1024 and increments theunitIndex, effectively converting the bytes to the appropriate unit. - Format and Return: Finally, it formats the
valueto one decimal place and concatenates it with the corresponding unit from theunitsarray, returning the formatted string.
By including formatBytes in the centralized utility module, we further reduce code duplication and promote consistency in the codebase.
Implementing the Solution: A Step-by-Step Guide
With the problem defined and the solution outlined, let's delve into the practical steps of implementing the refactoring.
1. Create the Centralized Utility Module
The first step is to create the lib/utils/time-formatting.ts file and add the formatWatchTime and formatBytes functions:
// lib/utils/time-formatting.ts
/**
* Formats duration in minutes to human-readable string
* @param minutes - Duration in minutes
* @returns Formatted string like "2d 5h 30m" or "45m"
*/
export function formatWatchTime(minutes: number): string {
const days = Math.floor(minutes / (24 * 60))
const hours = Math.floor((minutes % (24 * 60)) / 60)
const mins = Math.floor(minutes % 60)
const parts: string[] = []
if (days > 0) parts.push(`${days}d`)
if (hours > 0) parts.push(`${hours}h`)
if (mins > 0 || parts.length === 0) parts.push(`${mins}m`)
return parts.join(' ')
}
/**
* Formats bytes to human-readable string
* @param bytes - Size in bytes
* @returns Formatted string like "1.5 GB" or "256 MB"
*/
export function formatBytes(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let unitIndex = 0
let value = bytes
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024
unitIndex++
}
return `${value.toFixed(1)} ${units[unitIndex]}`
}
This module now serves as the single source of truth for these formatting functions.
2. Import and Replace Duplicated Code
The next step is to go through each of the identified duplicate locations and replace the existing implementations with imports from the new utility module. For example, in components/admin/prompts/prompt-template-editor.tsx, you would:
- Import the function: Add the following import statement at the top of the file:
import { formatWatchTime } from 'lib/utils/time-formatting'; - Replace the existing implementation: Locate the duplicated
formatWatchTimefunction and remove it. Then, update any calls to the function to use the imported version.
Repeat this process for all identified locations, including:
components/wrapped/wrapped-sections/utils.tscomponents/wrapped/wrapped-sections/top-movies-section.tsxcomponents/wrapped/wrapped-sections/total-watch-time-section.tsxcomponents/wrapped/wrapped-sections/shows-breakdown-section.tsxcomponents/wrapped/wrapped-sections/movies-breakdown-section.tsxdeletion-history-client.tsx(forformatBytes)
3. Add Unit Tests
To ensure the correctness and robustness of the formatting functions, it's crucial to add unit tests. These tests will verify that the functions produce the expected output for various inputs. Create a test file (e.g., lib/utils/time-formatting.test.ts) and add tests for both formatWatchTime and formatBytes. For example:
// lib/utils/time-formatting.test.ts
import { formatWatchTime, formatBytes } from './time-formatting';
describe('formatWatchTime', () => {
it('should format minutes correctly', () => {
expect(formatWatchTime(150)).toBe('2h 30m');
expect(formatWatchTime(1440)).toBe('1d');
expect(formatWatchTime(3456)).toBe('2d 10h 16m');
expect(formatWatchTime(45)).toBe('45m');
});
});
describe('formatBytes', () => {
it('should format bytes correctly', () => {
expect(formatBytes(1024)).toBe('1.0 KB');
expect(formatBytes(1048576)).toBe('1.0 MB');
expect(formatBytes(16777216)).toBe('16.0 MB');
});
});
These tests cover a range of inputs and ensure that the functions behave as expected. Running these tests after any modifications to the functions will help prevent regressions.
4. Verify and Test the Changes
After replacing the duplicated code and adding unit tests, it's essential to thoroughly verify the changes. Run the application and ensure that the formatting functions are working correctly in all relevant components. Pay close attention to the areas where the formatWatchTime and formatBytes functions are used.
5. Commit the Changes
Once you are confident that the changes are correct and the application is functioning as expected, commit the changes to your version control system. This will preserve the refactored code and allow you to track any future modifications.
Acceptance Criteria: Ensuring Success
To ensure that the refactoring is successful, the following acceptance criteria should be met:
- Single Source of Truth: The
formatWatchTimeandformatBytesfunctions should exist only in thelib/utils/time-formatting.tsfile. - Centralized Imports: All components that require these functions should import them from the centralized utility module.
- Unit Tests: Comprehensive unit tests should be in place to verify the correctness of the formatting functions.
- No Duplication: All duplicate implementations of the functions should be removed from the codebase.
By adhering to these criteria, we can confidently say that the refactoring has been successful in eliminating code duplication and improving the overall quality of the codebase.
Conclusion: The Benefits of Centralization
Refactoring to centralize the formatWatchTime and formatBytes utilities demonstrates the tangible benefits of adhering to the DRY principle. By eliminating code duplication, we have:
- Reduced Maintenance Burden: Changes to the formatting logic need only be made in one place, simplifying maintenance and reducing the risk of inconsistencies.
- Improved Readability: The codebase is cleaner and easier to understand, as the formatting logic is now encapsulated in a single module.
- Enhanced Testability: The centralized functions are easier to test, as we can focus our efforts on the utility module.
- Increased Code Reuse: The formatting functions can be easily reused across the application, promoting consistency and reducing development time.
This refactoring effort not only improves the current state of the codebase but also sets a positive precedent for future development. By actively addressing code duplication and promoting code reuse, we can build more maintainable, robust, and efficient applications.
For further reading on refactoring and code quality, consider exploring resources like Martin Fowler's Refactoring book. It provides in-depth guidance on various refactoring techniques and principles.