001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info 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 java.io.Serializable;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025import javax.annotation.Nullable;
026import org.apache.commons.lang.StringUtils;
027import org.apache.commons.lang.builder.ToStringBuilder;
028import org.apache.commons.lang.builder.ToStringStyle;
029
030/**
031 * @since 4.3
032 */
033public class Duration implements Serializable {
034
035  public static final String DAY = "d";
036  public static final String HOUR = "h";
037  public static final String MINUTE = "min";
038
039  private static final short MINUTES_IN_ONE_HOUR = 60;
040
041  private final long durationInMinutes;
042
043  private Duration(long durationInMinutes) {
044    this.durationInMinutes = durationInMinutes;
045  }
046
047  private Duration(int days, int hours, int minutes, int hoursInDay) {
048    this(((long) days * hoursInDay * MINUTES_IN_ONE_HOUR) + (hours * MINUTES_IN_ONE_HOUR) + minutes);
049  }
050
051  /**
052   * Create a Duration from a number of minutes.
053   */
054  public static Duration create(long durationInMinutes) {
055    return new Duration(durationInMinutes);
056  }
057
058  /**
059   * Create a Duration from a text duration and the number of hours in a day.
060   * <br>
061   * For instance, Duration.decode("1d 1h", 8) will have a number of minutes of 540 (1*8*60 + 60).
062   * */
063  public static Duration decode(String text, int hoursInDay) {
064    int days = 0;
065    int hours = 0;
066    int minutes = 0;
067    String sanitizedText = StringUtils.deleteWhitespace(text);
068    Pattern pattern = Pattern.compile("\\s*+(?:(\\d++)\\s*+" + DAY + ")?+\\s*+(?:(\\d++)\\s*+" + HOUR + ")?+\\s*+(?:(\\d++)\\s*+" + MINUTE + ")?+\\s*+");
069    Matcher matcher = pattern.matcher(text);
070
071    try {
072      if (matcher.find()) {
073        String daysDuration = matcher.group(1);
074        if (daysDuration != null) {
075          days = Integer.parseInt(daysDuration);
076          sanitizedText = sanitizedText.replace(daysDuration + DAY, "");
077        }
078        String hoursText = matcher.group(2);
079        if (hoursText != null) {
080          hours = Integer.parseInt(hoursText);
081          sanitizedText = sanitizedText.replace(hoursText + HOUR, "");
082        }
083        String minutesText = matcher.group(3);
084        if (minutesText != null) {
085          minutes = Integer.parseInt(minutesText);
086          sanitizedText = sanitizedText.replace(minutesText + MINUTE, "");
087        }
088        if (sanitizedText.isEmpty()) {
089          return new Duration(days, hours, minutes, hoursInDay);
090        }
091      }
092      throw invalid(text, null);
093    } catch (NumberFormatException e) {
094      throw invalid(text, e);
095    }
096  }
097
098  /**
099   * Return the duration in text, by using the given hours in day parameter to process hours.
100   * <br>
101   * Duration.decode("1d 1h", 8).encode(8) will return "1d 1h"
102   * Duration.decode("2d", 8).encode(16) will return "1d"
103   */
104  public String encode(int hoursInDay) {
105    int days = ((Double) ((double) durationInMinutes / hoursInDay / MINUTES_IN_ONE_HOUR)).intValue();
106    Long remainingDuration = durationInMinutes - (days * hoursInDay * MINUTES_IN_ONE_HOUR);
107    int hours = ((Double) (remainingDuration.doubleValue() / MINUTES_IN_ONE_HOUR)).intValue();
108    remainingDuration = remainingDuration - (hours * MINUTES_IN_ONE_HOUR);
109    int minutes = remainingDuration.intValue();
110
111    StringBuilder stringBuilder = new StringBuilder();
112    if (days > 0) {
113      stringBuilder.append(days);
114      stringBuilder.append(DAY);
115    }
116    if (hours > 0) {
117      stringBuilder.append(hours);
118      stringBuilder.append(HOUR);
119    }
120    if (minutes > 0) {
121      stringBuilder.append(minutes);
122      stringBuilder.append(MINUTE);
123    }
124    return stringBuilder.length() == 0 ? ("0" + MINUTE) : stringBuilder.toString();
125  }
126
127  /**
128   * Return the duration in minutes.
129   * <br>
130   * For instance, Duration.decode(1h, 24).toMinutes() will return 60.
131   */
132  public long toMinutes() {
133    return durationInMinutes;
134  }
135
136  /**
137   * Return true if the given duration is greater than the current one.
138   */
139  public boolean isGreaterThan(Duration other) {
140    return toMinutes() > other.toMinutes();
141  }
142
143  /**
144   * Add the given duration to the current one.
145   */
146  public Duration add(Duration with) {
147    return Duration.create(durationInMinutes + with.durationInMinutes);
148  }
149
150  /**
151   * Subtract the given duration to the current one.
152   */
153  public Duration subtract(Duration with) {
154    return Duration.create(durationInMinutes - with.durationInMinutes);
155  }
156
157  /**
158   * Multiply the duration with the given factor.
159   */
160  public Duration multiply(int factor) {
161    return Duration.create(durationInMinutes * factor);
162  }
163
164  private static IllegalArgumentException invalid(String text, @Nullable Exception e) {
165    throw new IllegalArgumentException(String.format("Duration '%s' is invalid, it should use the following sample format : 2d 10h 15min", text), e);
166  }
167
168  @Override
169  public boolean equals(Object o) {
170    if (this == o) {
171      return true;
172    }
173    if (o == null || getClass() != o.getClass()) {
174      return false;
175    }
176
177    Duration that = (Duration) o;
178    return durationInMinutes == that.durationInMinutes;
179
180  }
181
182  @Override
183  public int hashCode() {
184    return (int) (durationInMinutes ^ (durationInMinutes >>> 32));
185  }
186
187  @Override
188  public String toString() {
189    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
190  }
191}