001/*
002 * SonarQube
003 * Copyright (C) 2009-2016 SonarSource SA
004 * mailto:contact AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.utils;
021
022import org.apache.commons.lang.StringUtils;
023import org.apache.commons.lang.builder.ToStringBuilder;
024import org.apache.commons.lang.builder.ToStringStyle;
025
026import javax.annotation.Nullable;
027
028import java.io.Serializable;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031
032/**
033 * @since 4.3
034 */
035public class Duration implements Serializable {
036
037  public static final String DAY = "d";
038  public static final String HOUR = "h";
039  public static final String MINUTE = "min";
040
041  private static final short MINUTES_IN_ONE_HOUR = 60;
042
043  private final long durationInMinutes;
044
045  private Duration(long durationInMinutes) {
046    this.durationInMinutes = durationInMinutes;
047  }
048
049  private Duration(int days, int hours, int minutes, int hoursInDay) {
050    this((days * hoursInDay * MINUTES_IN_ONE_HOUR) + (hours * MINUTES_IN_ONE_HOUR) + minutes);
051  }
052
053  /**
054   * Create a Duration from a number of minutes.
055   */
056  public static Duration create(long durationInMinutes) {
057    return new Duration(durationInMinutes);
058  }
059
060  /**
061   * Create a Duration from a text duration and the number of hours in a day.
062   * <br>
063   * For instance, Duration.decode("1d 1h", 8) will have a number of minutes of 540 (1*8*60 + 60).
064   * */
065  public static Duration decode(String text, int hoursInDay) {
066    int days = 0;
067    int hours = 0;
068    int minutes = 0;
069    String sanitizedText = StringUtils.deleteWhitespace(text);
070    Pattern pattern = Pattern.compile("\\s*+(?:(\\d++)\\s*+" + DAY + ")?+\\s*+(?:(\\d++)\\s*+" + HOUR + ")?+\\s*+(?:(\\d++)\\s*+" + MINUTE + ")?+\\s*+");
071    Matcher matcher = pattern.matcher(text);
072
073    try {
074      if (matcher.find()) {
075        String daysDuration = matcher.group(1);
076        if (daysDuration != null) {
077          days = Integer.parseInt(daysDuration);
078          sanitizedText = sanitizedText.replace(daysDuration + DAY, "");
079        }
080        String hoursText = matcher.group(2);
081        if (hoursText != null) {
082          hours = Integer.parseInt(hoursText);
083          sanitizedText = sanitizedText.replace(hoursText + HOUR, "");
084        }
085        String minutesText = matcher.group(3);
086        if (minutesText != null) {
087          minutes = Integer.parseInt(minutesText);
088          sanitizedText = sanitizedText.replace(minutesText + MINUTE, "");
089        }
090        if (sanitizedText.isEmpty()) {
091          return new Duration(days, hours, minutes, hoursInDay);
092        }
093      }
094      throw invalid(text, null);
095    } catch (NumberFormatException e) {
096      throw invalid(text, e);
097    }
098  }
099
100  /**
101   * Return the duration in text, by using the given hours in day parameter to process hours.
102   * <br/>
103   * Duration.decode("1d 1h", 8).encode(8) will return "1d 1h"
104   * Duration.decode("2d", 8).encode(16) will return "1d"
105   */
106  public String encode(int hoursInDay) {
107    int days = ((Double) ((double) durationInMinutes / hoursInDay / MINUTES_IN_ONE_HOUR)).intValue();
108    Long remainingDuration = durationInMinutes - (days * hoursInDay * MINUTES_IN_ONE_HOUR);
109    int hours = ((Double) (remainingDuration.doubleValue() / MINUTES_IN_ONE_HOUR)).intValue();
110    remainingDuration = remainingDuration - (hours * MINUTES_IN_ONE_HOUR);
111    int minutes = remainingDuration.intValue();
112
113    StringBuilder stringBuilder = new StringBuilder();
114    if (days > 0) {
115      stringBuilder.append(days);
116      stringBuilder.append(DAY);
117    }
118    if (hours > 0) {
119      stringBuilder.append(hours);
120      stringBuilder.append(HOUR);
121    }
122    if (minutes > 0) {
123      stringBuilder.append(minutes);
124      stringBuilder.append(MINUTE);
125    }
126    return stringBuilder.toString();
127  }
128
129  /**
130   * Return the duration in minutes.
131   * <br>
132   * For instance, Duration.decode(1h, 24).toMinutes() will return 60.
133   */
134  public long toMinutes() {
135    return durationInMinutes;
136  }
137
138  /**
139   * Return true if the given duration is greater than the current one.
140   */
141  public boolean isGreaterThan(Duration other) {
142    return toMinutes() > other.toMinutes();
143  }
144
145  /**
146   * Add the given duration to the current one.
147   */
148  public Duration add(Duration with) {
149    return Duration.create(durationInMinutes + with.durationInMinutes);
150  }
151
152  /**
153   * Subtract the given duration to the current one.
154   */
155  public Duration subtract(Duration with) {
156    return Duration.create(durationInMinutes - with.durationInMinutes);
157  }
158
159  /**
160   * Multiply the duration with the given factor.
161   */
162  public Duration multiply(int factor) {
163    return Duration.create(durationInMinutes * factor);
164  }
165
166  private static IllegalArgumentException invalid(String text, @Nullable Exception e) {
167    throw new IllegalArgumentException(String.format("Duration '%s' is invalid, it should use the following sample format : 2d 10h 15min", text), e);
168  }
169
170  @Override
171  public boolean equals(Object o) {
172    if (this == o) {
173      return true;
174    }
175    if (o == null || getClass() != o.getClass()) {
176      return false;
177    }
178
179    Duration that = (Duration) o;
180    return durationInMinutes == that.durationInMinutes;
181
182  }
183
184  @Override
185  public int hashCode() {
186    return (int) (durationInMinutes ^ (durationInMinutes >>> 32));
187  }
188
189  @Override
190  public String toString() {
191    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
192  }
193}