BookArrayUtils.java

package com.university.bookstore.utils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Stream;

import com.university.bookstore.model.Book;

/**
 * Utility class for array-based operations on Book objects.
 * 
 * <p>This class provides static methods for manipulating and analyzing
 * arrays of books, demonstrating array operations without using ArrayList.</p>
 * 
 * <p>All methods handle null arrays and null elements gracefully.</p>
 * 
 * @author Navid Mohaghegh
 * @version 1.0
 * @since 2024-09-15
 */
public final class BookArrayUtils {
    
    /**
     * Private constructor to prevent instantiation.
     */
    private BookArrayUtils() {
        throw new UnsupportedOperationException("Utility class cannot be instantiated");
    }
    
    /**
     * Counts books published before a given year.
     * 
     * @param books array of books (may be null or contain nulls)
     * @param yearCutoff the cutoff year (exclusive)
     * @return count of books published before the cutoff year
     */
    public static int countBeforeYear(Book[] books, int yearCutoff) {
        if (books == null) {
            return 0;
        }
        
        int count = 0;
        for (Book book : books) {
            if (book != null && book.getYear() < yearCutoff) {
                count++;
            }
        }
        return count;
    }
    
    /**
     * Counts books by a specific author (case-insensitive, exact match).
     * 
     * @param books array of books (may be null or contain nulls)
     * @param author the author name to search for
     * @return count of books by the specified author
     */
    public static int countByAuthor(Book[] books, String author) {
        if (books == null || author == null) {
            return 0;
        }
        
        int count = 0;
        for (Book book : books) {
            if (book != null && book.getAuthor().equalsIgnoreCase(author)) {
                count++;
            }
        }
        return count;
    }
    
    /**
     * Filters books with price at most the specified maximum.
     * Returns a compact array (no nulls, exact size).
     * 
     * @param books array of books (may be null or contain nulls)
     * @param maxPrice maximum price (inclusive)
     * @return compact array of books with price less than or equal to maxPrice
     * @throws IllegalArgumentException if maxPrice is negative
     */
    public static Book[] filterPriceAtMost(Book[] books, double maxPrice) {
        if (maxPrice < 0) {
            throw new IllegalArgumentException("Max price cannot be negative");
        }
        
        if (books == null) {
            return new Book[0];
        }
        
        // Count matching books
        int count = 0;
        for (Book book : books) {
            if (book != null && book.getPrice() <= maxPrice) {
                count++;
            }
        }
        
        // Create compact array
        Book[] result = new Book[count];
        int index = 0;
        for (Book book : books) {
            if (book != null && book.getPrice() <= maxPrice) {
                result[index++] = book;
            }
        }
        
        return result;
    }
    
    /**
     * Filters books published in a specific decade.
     * For example, decade 1990 includes years 1990-1999.
     * 
     * @param books array of books (may be null or contain nulls)
     * @param decade the decade start year (e.g., 1990, 2000)
     * @return compact array of books from that decade
     */
    public static Book[] filterByDecade(Book[] books, int decade) {
        if (books == null) {
            return new Book[0];
        }
        
        int decadeEnd = decade + 9;
        
        // Count matching books
        int count = 0;
        for (Book book : books) {
            if (book != null && book.getYear() >= decade && book.getYear() <= decadeEnd) {
                count++;
            }
        }
        
        // Create compact array
        Book[] result = new Book[count];
        int index = 0;
        for (Book book : books) {
            if (book != null && book.getYear() >= decade && book.getYear() <= decadeEnd) {
                result[index++] = book;
            }
        }
        
        return result;
    }
    
    /**
     * Sorts books by price in ascending order (in-place).
     * Nulls are moved to the end.
     * 
     * @param books array to sort (modified in-place)
     */
    public static void sortByPrice(Book[] books) {
        if (books == null || books.length <= 1) {
            return;
        }
        
        Arrays.sort(books, (a, b) -> {
            if (a == null && b == null) return 0;
            if (a == null) return 1;
            if (b == null) return -1;
            return Double.compare(a.getPrice(), b.getPrice());
        });
    }
    
    /**
     * Sorts books by year in ascending order (in-place).
     * Nulls are moved to the end.
     * 
     * @param books array to sort (modified in-place)
     */
    public static void sortByYear(Book[] books) {
        if (books == null || books.length <= 1) {
            return;
        }
        
        Arrays.sort(books, (a, b) -> {
            if (a == null && b == null) return 0;
            if (a == null) return 1;
            if (b == null) return -1;
            return Integer.compare(a.getYear(), b.getYear());
        });
    }
    
    /**
     * Calculates the average price of books in the array.
     * 
     * @param books array of books (may be null or contain nulls)
     * @return average price, or 0.0 if array is null or empty
     */
    public static double averagePrice(Book[] books) {
        if (books == null) {
            return 0.0;
        }
        
        double sum = 0.0;
        int count = 0;
        
        for (Book book : books) {
            if (book != null) {
                sum += book.getPrice();
                count++;
            }
        }
        
        return count == 0 ? 0.0 : sum / count;
    }
    
    /**
     * Finds the oldest book (earliest publication year).
     * 
     * @param books array of books (may be null or contain nulls)
     * @return the oldest book, or null if array is null/empty
     */
    public static Book findOldest(Book[] books) {
        if (books == null) {
            return null;
        }
        
        Book oldest = null;
        for (Book book : books) {
            if (book != null) {
                if (oldest == null || book.getYear() < oldest.getYear()) {
                    oldest = book;
                }
            }
        }
        
        return oldest;
    }
    
    /**
     * Merges two book arrays into one, preserving all elements.
     * 
     * @param arr1 first array (may be null)
     * @param arr2 second array (may be null)
     * @return merged array containing all books from both arrays
     */
    public static Book[] merge(Book[] arr1, Book[] arr2) {
        int len1 = (arr1 == null) ? 0 : arr1.length;
        int len2 = (arr2 == null) ? 0 : arr2.length;
        
        Book[] result = new Book[len1 + len2];
        
        if (arr1 != null) {
            System.arraycopy(arr1, 0, result, 0, len1);
        }
        if (arr2 != null) {
            System.arraycopy(arr2, 0, result, len1, len2);
        }
        
        return result;
    }
    
    /**
     * Removes duplicate books based on ISBN.
     * Returns a compact array with unique books only.
     * 
     * @param books array of books (may be null or contain nulls)
     * @return compact array with duplicates removed
     */
    public static Book[] removeDuplicates(Book[] books) {
        if (books == null) {
            return new Book[0];
        }
        
        Set<String> seenIsbns = new HashSet<>();
        List<Book> unique = new ArrayList<>();
        
        for (Book book : books) {
            if (book != null && seenIsbns.add(book.getIsbn())) {
                unique.add(book);
            }
        }
        
        return unique.toArray(new Book[0]);
    }
    
    /**
     * Finds books within a year range (inclusive).
     * 
     * @param books array of books
     * @param startYear start year (inclusive)
     * @param endYear end year (inclusive)
     * @return compact array of books within the year range
     */
    public static Book[] filterByYearRange(Book[] books, int startYear, int endYear) {
        if (books == null || startYear > endYear) {
            return new Book[0];
        }
        
        return Stream.of(books)
            .filter(book -> book != null && 
                    book.getYear() >= startYear && 
                    book.getYear() <= endYear)
            .toArray(Book[]::new);
    }
    
    /**
     * Groups books by decade and returns a summary.
     * 
     * @param books array of books
     * @return map with decade as key and count as value
     */
    public static Map<Integer, Integer> countByDecade(Book[] books) {
        Map<Integer, Integer> decadeCounts = new TreeMap<>();
        
        if (books != null) {
            for (Book book : books) {
                if (book != null) {
                    int decade = (book.getYear() / 10) * 10;
                    decadeCounts.merge(decade, 1, Integer::sum);
                }
            }
        }
        
        return decadeCounts;
    }
    
    /**
     * Finds the book with the longest title.
     * 
     * @param books array of books
     * @return book with longest title, null if array is null/empty
     */
    public static Book findLongestTitle(Book[] books) {
        if (books == null) {
            return null;
        }
        
        Book longest = null;
        int maxLength = 0;
        
        for (Book book : books) {
            if (book != null && book.getTitle().length() > maxLength) {
                maxLength = book.getTitle().length();
                longest = book;
            }
        }
        
        return longest;
    }
}