Sunday, September 14, 2014

Age implementation in Java

Few days ago I saw a post in StackoverFlow asking for finding date of birth from age in java. I thought that why don't I write a class which can manipulate Age. Here is what I came up with.
This class Age stores age in terms of

  • Years
  • Months
  • Days
  • Hours
  • Minutes
  • Seconds

Followings are the features of this class:

  • It returns Age for a given Date
  • It is Timezone specific
  • It returns age as Seconds, Minutes, Hours, Days, Weeks, Months
  • Prints Age in a pretty format
  • This class is Serializable, Cloneable and Comparable
  • It is written using java.time api

import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.ValueRange;
import java.util.Date;
import java.util.Objects;

/**
 * The class <code>Age</code> represents the length of time that a person has
 * lived or a thing has existed.
 * 
 * @author TapasB
 */
public class Age implements Serializable, Cloneable, Comparable<Age> {

 private static final long serialVersionUID = 210032925430172448L;

 private long years;
 private long months;
 private long days;
 private long hours;
 private long minutes;
 private long seconds;

 // Private Constructor
 private Age() {

 }

 /**
  * Creates and returns a new instance of <code>Age</code> object.
  * 
  * @author TapasB
  * @return a new instance of <code>Age</code>
  */
 public static Age getDefault() {
  return new Age();
 }

 /**
  * Creates and returns a new instance of <code>Age</code> object after
  * setting the given years.
  * 
  * @author TapasB
  * @param years
  *            - years to set
  * @return a new instance of <code>Age</code>
  */
 public static Age of(long years) {
  return getDefault().setYears(years);
 }

 /**
  * Creates and returns a new instance of <code>Age</code> object after
  * setting given years and months to it.
  * 
  * @author TapasB
  * @param years
  *            - years to set
  * @param months
  *            - months to set
  * @return a new instance of <code>Age</code>
  */
 public static Age of(long years, long months) {
  return of(years).setMonths(months);
 }

 /**
  * Creates and returns a new instance of <code>Age</code> object after
  * setting given years, months and days to it.
  * 
  * @author TapasB
  * @param years
  *            - years to set
  * @param months
  *            - months to set
  * @param days
  *            - days to set
  * @return a new instance of <code>Age</code>
  */
 public static Age of(long years, long months, long days) {
  return of(years, months).setDays(days);
 }

 /**
  * Creates and returns a new instance of <code>Age</code> object after
  * setting given years, months, days and hours to it.
  * 
  * @author TapasB
  * @param years
  *            - years to set
  * @param months
  *            - months to set
  * @param days
  *            - days to set
  * @param hours
  *            - hours to set
  * @return a new instance of <code>Age</code>
  */
 public static Age of(long years, long months, long days, long hours) {
  return of(years, months, days).setHours(hours);
 }

 /**
  * Creates and returns a new instance of <code>Age</code> object after
  * setting given years, months, days, hours and minutes to it.
  * 
  * @author TapasB
  * @param years
  *            - years to set
  * @param months
  *            - months to set
  * @param days
  *            - days to set
  * @param hours
  *            - hours to set
  * @param minutes
  *            - minutes to set
  * @return a new instance of <code>Age</code>
  */
 public static Age of(long years, long months, long days, long hours, long minutes) {
  return of(years, months, days, hours).setMinutes(minutes);
 }

 /**
  * Creates and returns a new instance of <code>Age</code> object after
  * setting given years, months, days, hours, minutes and seconds to it.
  * 
  * @author TapasB
  * @param years
  *            - years to set
  * @param months
  *            - months to set
  * @param days
  *            - days to set
  * @param hours
  *            - hours to set
  * @param minutes
  *            - minutes to set
  * @param seconds
  *            - seconds to set
  * @return a new instance of <code>Age</code>
  */
 public static Age of(long years, long months, long days, long hours, long minutes, long seconds) {
  return of(years, months, days, hours, minutes).setSeconds(seconds);
 }

 /**
  * Calculates and returns an instance <code>Age</code> for the given date
  * string and date pattern. The <code>Age</code> will be calculated with
  * respect to the system default time-zone.
  * 
  * @author TapasB
  * @param date
  *            - the birth date as string
  * @param pattern
  *            - date pattern
  * @return an instance of <code>Age</code>
  * @throws ParseException
  *             if the parsing fails
  */
 public static Age fromDateOfBirth(String date, String pattern) throws ParseException {
  Objects.requireNonNull(date, "date");
  Objects.requireNonNull(pattern, "pattern");
  SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
  return fromDateOfBirth(dateFormat.parse(date));
 }

 /**
  * Calculates and returns an instance of <code>Age</code> for the given
  * {@link Date}. The <code>Age</code> will be calculated with respect to the
  * system default time-zone.
  * 
  * @author TapasB
  * @param date
  *            - the birth date
  * @return an instance of <code>Age</code>
  */
 public static Age fromDateOfBirth(Date date) {
  Objects.requireNonNull(date, "date");
  return fromDateOfBirth(date, ZoneId.systemDefault());
 }

 /**
  * Calculates and returns an instance of <code>Age</code> for the given
  * {@link Date} with respect to the given {@link ZoneId}.
  * 
  * @author TapasB
  * @param date
  *            - the birth date
  * @param zone
  *            - the zone ID to use
  * @return an instance of <code>Age</code>
  */
 public static Age fromDateOfBirth(Date date, ZoneId zone) {
  Objects.requireNonNull(date, "date");
  Objects.requireNonNull(zone, "zone");
  Instant instant = Instant.ofEpochMilli(date.getTime());
  LocalDateTime dateTime = LocalDateTime.ofInstant(instant, zone);
  return fromDateOfBirth(dateTime, zone);
 }

 /**
  * Calculates and returns an instance of <code>Age</code> for the given
  * {@link LocalDateTime}. The <code>Age</code> will be calculated with
  * respect to the system default time-zone.
  * 
  * @author TapasB
  * @param date
  *            - the birth date
  * @return an instance of <code>Age</code>
  */
 public static Age fromDateOfBirth(LocalDateTime date) {
  Objects.requireNonNull(date, "date");
  return fromDateOfBirth(date, ZoneId.systemDefault());
 }

 /**
  * Calculates and returns an instance of <code>Age</code> for the given
  * {@link LocalDateTime} with respect to the given {@link ZoneId}.
  * 
  * @author TapasB
  * @param date
  *            - the birth date
  * @param zone
  *            - the zone ID to use
  * @return an instance of <code>Age</code>
  */
 public static Age fromDateOfBirth(LocalDateTime date, ZoneId zone) {
  Objects.requireNonNull(date, "date");
  Objects.requireNonNull(zone, "zone");
  ZonedDateTime zonedDate = date.atZone(zone);
  return fromDateOfBirth(zonedDate);
 }

 /**
  * Calculates and returns an instance of <code>Age</code> for the given
  * {@link ZonedDateTime}.
  * 
  * @author TapasB
  * @param date
  *            - the birth date
  * @return an instance of <code>Age</code>
  */
 public static Age fromDateOfBirth(ZonedDateTime date) {
  Objects.requireNonNull(date, "date");
  ZonedDateTime now = ZonedDateTime.now(date.getZone());
  ZonedDateTime tempDate = ZonedDateTime.from(date);

  long years = tempDate.until(now, ChronoUnit.YEARS);
  tempDate = tempDate.plusYears(years);

  long months = tempDate.until(now, ChronoUnit.MONTHS);
  tempDate = tempDate.plusMonths(months);

  long days = tempDate.until(now, ChronoUnit.DAYS);
  tempDate = tempDate.plusDays(days);

  long hours = tempDate.until(now, ChronoUnit.HOURS);
  tempDate = tempDate.plusHours(hours);

  long minutes = tempDate.until(now, ChronoUnit.MINUTES);
  tempDate = tempDate.plusMinutes(minutes);

  long seconds = tempDate.until(now, ChronoUnit.SECONDS);

  Age age = getDefault();

  if (years > 0) {
   age.years = years;
  }

  if (months > 0) {
   age.months = months;
  }

  if (days > 0) {
   age.days = days;
  }

  if (hours > 0) {
   age.hours = hours;
  }

  if (minutes > 0) {
   age.minutes = minutes;
  }

  if (seconds > 0) {
   age.seconds = seconds;
  }

  return age;
 }

 /**
  * Sets the given years to this object.
  * 
  * @author TapasB
  * @param years
  *            - years to set
  * @return this instance for chain call
  */
 public Age setYears(long years) {
  checkValidValue("years", 
    ValueRange.of(0, ChronoField.YEAR.range().getMaximum()), 
    years);
  this.years = years;
  return this;
 }

 /**
  * Sets the given months to this object.
  * 
  * @author TapasB
  * @param months
  *            - months to set
  * @return this instance for chain call
  */
 public Age setMonths(long months) {
  checkValidValue("months", 
    ValueRange.of(0, ChronoField.MONTH_OF_YEAR.range().getMaximum()), 
    months);
  this.months = months;
  return this;
 }

 /**
  * Sets the given days to this object.
  * 
  * @author TapasB
  * @param days
  *            - days to set
  * @return this instance for chain call
  */
 public Age setDays(long days) {
  checkValidValue("days", 
    ValueRange.of(0, ChronoField.DAY_OF_MONTH.range().getMaximum()), 
    days);
  this.days = days;
  return this;
 }

 /**
  * Sets the given hours to this object.
  * 
  * @author TapasB
  * @param hours
  *            - hours to set
  * @return this instance for chain call
  */
 public Age setHours(long hours) {
  checkValidValue("hours", 
    ValueRange.of(0, ChronoField.HOUR_OF_DAY.range().getMaximum()), 
    hours);
  this.hours = hours;
  return this;
 }

 /**
  * Sets the given minutes to this object.
  * 
  * @author TapasB
  * @param minutes
  *            - minutes to set
  * @return this instance for chain call
  */
 public Age setMinutes(long minutes) {
  checkValidValue("minutes", 
    ValueRange.of(0, ChronoField.MINUTE_OF_HOUR.range().getMaximum()), 
    minutes);
  this.minutes = minutes;
  return this;
 }

 /**
  * Sets the given seconds to this object.
  * 
  * @author TapasB
  * @param seconds
  *            - seconds to set
  * @return this instance for chain call
  */
 public Age setSeconds(long seconds) {
  checkValidValue("seconds", 
    ValueRange.of(0, ChronoField.SECOND_OF_MINUTE.range().getMaximum()), 
    seconds);
  this.seconds = seconds;
  return this;
 }

 /**
  * Sets the given value to this object to the given field defined by
  * {@link ChronoUnit}.
  * 
  * <p>
  * The accepted list of values are:
  * <ul>
  * <li>ChronoUnit.YEARS</li>
  * <li>ChronoUnit.MONTHS</li>
  * <li>ChronoUnit.DAYS</li>
  * <li>ChronoUnit.HOURS</li>
  * <li>ChronoUnit.MINUTES</li>
  * <li>ChronoUnit.SECONDS</li>
  * </ul>
  * </p>
  * 
  * @author TapasB
  * @param value
  *            - value to set
  * @param unit
  *            - field to which the value needs to be set
  * @return this instance for chain call
  * @throws UnsupportedOperationException
  *             if the unit passed to it is not belongs to the allowed list
  *             of values
  */
 public Age set(long value, ChronoUnit unit) throws UnsupportedOperationException {
  Objects.requireNonNull(unit, "unit");

  switch (unit) {
   case YEARS:
    return setYears(value);

   case MONTHS:
    return setMonths(value);

   case DAYS:
    return setDays(value);

   case HOURS:
    return setHours(value);

   case MINUTES:
    return setMinutes(value);

   case SECONDS:
    return setSeconds(value);

   default:
    throw new UnsupportedOperationException("Unit " + unit + " not supported");
  }
 }

 /**
  * Calculates the <code>Age</code> and returns the birth date as
  * {@link LocalDateTime}.
  * 
  * @author TapasB
  * @return date of birth as <code>LocalDateTime</code>
  */
 public LocalDateTime getDateOfBirth() {
  LocalDateTime now = LocalDateTime.now();
  LocalDateTime dob = now.minus(this.years, ChronoUnit.YEARS).
    minus(this.months, ChronoUnit.MONTHS).
    minus(this.days, ChronoUnit.DAYS).
    minus(this.hours, ChronoUnit.HOURS).
    minus(this.minutes, ChronoUnit.MINUTES).
    minus(this.seconds, ChronoUnit.SECONDS);

  return dob;
 }

 /**
  * Calculates the <code>Age</code> and returns the birth date as
  * {@link ZonedDateTime} with respect to the given {@link ZoneId}.
  * 
  * @author TapasB
  * @param zone
  *            - the zone ID to use
  * @return date of birth as <code>ZonedDateTime</code>
  */
 public ZonedDateTime getDateOfBirth(ZoneId zone) {
  ZonedDateTime now = ZonedDateTime.now(zone);
  ZonedDateTime dob = now.minus(this.years, ChronoUnit.YEARS).
    minus(this.months, ChronoUnit.MONTHS).
    minus(this.days, ChronoUnit.DAYS).
    minus(this.hours, ChronoUnit.HOURS).
    minus(this.minutes, ChronoUnit.MINUTES).
    minus(this.seconds, ChronoUnit.SECONDS);

  return dob;
 }

 /**
  * Returns elapsed seconds from date of birth for this <code>Age</code>.
  * 
  * @author TapasB
  * @return seconds
  */
 public long asSeconds() {
  return asSeconds(ZoneId.systemDefault());
 }

 /**
  * Returns elapsed seconds from date of birth for this <code>Age</code> for
  * the given. zone
  * 
  * @author TapasB
  * @param zone
  *            - the zone ID to use
  * @return seconds
  */
 public long asSeconds(ZoneId zone) {
  return calculateDifference(ChronoUnit.SECONDS, zone);
 }

 /**
  * Returns elapsed minutes from date of birth for this <code>Age</code>.
  * 
  * @author TapasB
  * @return minutes
  */
 public long asMinutes() {
  return asMinutes(ZoneId.systemDefault());
 }

 /**
  * Returns elapsed minutes from date of birth for this <code>Age</code> for
  * the given zone.
  * 
  * @author TapasB
  * @param zone
  *            - the zone ID to use
  * @return minutes
  */
 public long asMinutes(ZoneId zone) {
  return calculateDifference(ChronoUnit.MINUTES, zone);
 }

 /**
  * Returns elapsed hours from date of birth for this <code>Age</code>.
  * 
  * @author TapasB
  * @return hours
  */
 public long asHours() {
  return asHours(ZoneId.systemDefault());
 }

 /**
  * Returns elapsed hours from date of birth for this <code>Age</code> for
  * the given zone.
  * 
  * @author TapasB
  * @param zone
  *            - the zone ID to use
  * @return hours
  */
 public long asHours(ZoneId zone) {
  return calculateDifference(ChronoUnit.HOURS, zone);
 }

 /**
  * Returns elapsed days from date of birth for this <code>Age</code>.
  * 
  * @author TapasB
  * @return days
  */
 public long asDays() {
  return asDays(ZoneId.systemDefault());
 }

 /**
  * Returns elapsed days from date of birth for this <code>Age</code> for the
  * given zone.
  * 
  * @author TapasB
  * @param zone
  *            - the zone ID to use
  * @return days
  */
 public long asDays(ZoneId zone) {
  return calculateDifference(ChronoUnit.DAYS, zone);
 }

 /**
  * Returns elapsed weeks from date of birth for this <code>Age</code>.
  * 
  * @author TapasB
  * @return weeks
  */
 public long asWeeks() {
  return asWeeks(ZoneId.systemDefault());
 }

 /**
  * Returns elapsed weeks from date of birth for this <code>Age</code> for
  * the given zone.
  * 
  * @author TapasB
  * @param zone
  *            - the zone ID to use
  * @return weeks
  */
 public long asWeeks(ZoneId zone) {
  return calculateDifference(ChronoUnit.WEEKS, zone);
 }

 /**
  * Returns elapsed months from date of birth for this <code>Age</code>.
  * 
  * @author TapasB
  * @return months
  */
 public long asMonths() {
  return asMonths(ZoneId.systemDefault());
 }

 /**
  * Returns elapsed months from date of birth for this <code>Age</code> for
  * the given zone.
  * 
  * @author TapasB
  * @param zone
  *            - the zone ID to use
  * @return months
  */
 public long asMonths(ZoneId zone) {
  return calculateDifference(ChronoUnit.MONTHS, zone);
 }

 /**
  * Returns string representation of the <code>Age</code> in the following
  * format: <br/>
  * <code>A Years B Months C Days D Hours E Minutes F Seconds</code> <br/>
  * Any field except Seconds will be omitted if the value is 0.
  * 
  * @author TapasB
  * @return the string representation
  */
 public String getDisplayString() {
  StringBuilder builder = new StringBuilder();

  setString(years, builder, "Years");
  setString(months, builder, "Months");
  setString(days, builder, "Days");
  setString(hours, builder, "Hours");
  setString(minutes, builder, "Minutes");

  builder.append(seconds);
  builder.append(" Seconds.");

  return builder.toString();
 }

 /**
  * Returns string representation of the <code>Age</code> in the following
  * format: <br/>
  * <code>A Years B Months C Days D Hours E Minutes F Seconds</code> <br/>
  * Any field except Seconds will be omitted if the value is 0.
  * 
  * @author TapasB
  * @return the string representation
  */
 @Override
 public String toString() {
  return getDisplayString();
 }

 /**
  * Compares two <code>Age</code> objects.
  * 
  * @author TapasB
  * @param age
  *            - the other <code>Age</code> to be compared
  * @return the value 0 if the argument <code>Age</code> is equal to this
  *         <code>Age</code>; a value less than 0 if this <code>Age</code> is
  *         before the <code>Age</code> argument; and a value greater than 0
  *         if this <code>Age</code> is after the <code>Age</code> argument.
  */
 @Override
 public int compareTo(Age age) {
  Objects.requireNonNull(age, "age");
  long thisSeconds = asSeconds();
  long otherSeconds = age.asSeconds();
  return Long.compare(thisSeconds, otherSeconds);
 }

 /**
  * Creates and returns a copy of this object with the same years, months,
  * days, hours, minutes and seconds.
  * 
  * @author TapasB
  * @return a clone of this instance of <code>Age</code>
  */
 @Override
 protected Age clone() {
  return of(years, months, days, hours, minutes, seconds);
 }

 /**
  * Returns a hash code value for the <code>Age</code> object.
  * 
  * @author TapasB
  * @return a hash code value for this object
  */
 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  Long thisSeconds = asSeconds();
  int intValue = thisSeconds.intValue();
  result = prime * result + (int) (intValue ^ (intValue >>> 32));
  return result;
 }

 /**
  * Indicates whether some other <code>Age</code> object is same to this one.
  * 
  * @author TapasB
  * @param obj
  *            - the reference object with which to compare
  * @return true if this object is the same as the obj argument; false
  *         otherwise.
  * @see java.lang.Object#equals(java.lang.Object)
  */
 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }

  if (obj == null) {
   return false;
  }

  if (getClass() != obj.getClass()) {
   return false;
  }

  Age age = (Age) obj;
  return compareTo(age) == 0;
 }

 // Checks the validity of the field value
 private void checkValidValue(String field, ValueRange range, long value) {
  if (!range.isValidValue(value)) {
   throw new DateTimeException(getInvalidFieldMessage(field, range, value));
  }
 }

 // Returns message in case of invalid value which is used by
 // DateTimeException
 private String getInvalidFieldMessage(String field, ValueRange range, long value) {
  return "Invalid value " + value + " for " + field + ". Valid values: (" + range.getMinimum() + ", " + range.getMaximum() + ").";
 }

 // Appends the field value of Age to the StringBuilder with the token
 private void setString(long value, StringBuilder builder, String token) {
  if (value > 0) {
   builder.append(value);
   builder.append(" ").append(token).append(" ");
  }
 }

 // Calculates the amount of time with respect to the given ChronoUnit
 // and ZoneId between the birth date and now
 private long calculateDifference(ChronoUnit chronoUnit, ZoneId zone) {
  Objects.requireNonNull(chronoUnit);
  Objects.requireNonNull(zone);
  ZonedDateTime dob = getDateOfBirth(zone);
  ZonedDateTime now = ZonedDateTime.now(zone);
  return chronoUnit.between(dob, now);
 }
}
Few example code to use:
public static void getDateOfBirthTest() {
 Age age = Age.of(29, 8, 28, 12, 49, 0);
 System.out.println(age.getDateOfBirth());
}

public static void getAgeFromDateTest() {
 Age age = Age.fromDateOfBirth(LocalDateTime.of(1984, Month.DECEMBER, 16, 7, 45, 0));
 System.out.println(age);
}

public static void ageAsUnitsTest() {
 Age age = Age.fromDateOfBirth(LocalDateTime.of(1984, Month.DECEMBER, 16, 7, 45, 0));
 System.out.println(age.asWeeks());
}

@SuppressWarnings("resource")
public static void serializationTest() throws FileNotFoundException, IOException, ClassNotFoundException {
 Age age = Age.fromDateOfBirth(LocalDateTime.of(1984, Month.DECEMBER, 16, 7, 45));
 System.out.println(age);
 
 ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("C:/tmp")));
 outputStream.writeObject(age);
 outputStream.flush();
 
 ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("C:/tmp")));
 age = Age.class.cast(inputStream.readObject());
 System.out.println(age);
}

public static void equalsTest() {
 Age age = Age.of(29, 8, 28, 12, 49, 0);
 Age clone = age.clone();
 System.out.println(age.equals(clone));
}
While developing this class I was stuck to calculate the difference between to date-time and I got help from one SO user and I really appreciate that.
You comments and suggestion is appreciable to improve it.
Happy coding.

No comments:

Post a Comment