matrices - como usar calendario en java
¿Cómo ignoro los fines de semana usando el calendario de Java? (2)
Estoy tratando de obtener la cantidad de minutos entre dos instancias de tiempo en particular ignorando los fines de semana. Esto es lo que hice.
public static final List<Integer> NON_WORKING_DAYS;
static {
List<Integer> nonWorkingDays = new ArrayList<Integer>();
nonWorkingDays.add(Calendar.SATURDAY);
nonWorkingDays.add(Calendar.SUNDAY);
NON_WORKING_DAYS = Collections.unmodifiableList(nonWorkingDays);
}
public static int getMinsBetween(Date d1, Date d2, boolean onlyBusinessDays)
{
int minsBetween = (int)((d2.getTime() - d1.getTime()) / (1000 * 60));
int minsToSubtract = 0;
if(onlyBusinessDays){
Calendar dateToCheck = Calendar.getInstance();
dateToCheck.setTime(d1);
Calendar dateToCompare = Calendar.getInstance();
dateToCompare.setTime(d2);
//moving the first day of the week to Tues so that a Sat, sun and mon fall in the same week, easy to adjust dates
dateToCheck.setFirstDayOfWeek(Calendar.TUESDAY);
dateToCompare.setFirstDayOfWeek(Calendar.TUESDAY);
//moving the dates out of weekends
if(!isBusinessDay(dateToCheck, NON_WORKING_DAYS)){
dateToCheck.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
dateToCheck.set(Calendar.HOUR, 0);
dateToCheck.set(Calendar.MINUTE, 0);
dateToCheck.set(Calendar.SECOND, 0);
dateToCheck.set(Calendar.MILLISECOND, 0);
}
if(!isBusinessDay(dateToCompare, NON_WORKING_DAYS)){
dateToCompare.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
dateToCompare.set(Calendar.HOUR, 0);
dateToCompare.set(Calendar.MINUTE, 0);
dateToCompare.set(Calendar.SECOND, 0);
dateToCompare.set(Calendar.MILLISECOND, 0);
}
for(; dateToCheck.getTimeInMillis() < dateToCompare.getTimeInMillis() ; dateToCheck.add(Calendar.DAY_OF_MONTH, 1)){
if(isBusinessDay(dateToCheck, NON_WORKING_DAYS)){
minsToSubtract = minsToSubtract + 1440;
}
}
minsBetween = minsBetween - minsToSubtract;
}
return minsBetween;
}
private static boolean isBusinessDay(Calendar dateToCheck, List<Integer> daysToExclude){
for(Integer dayToExclude : daysToExclude){
if(dayToExclude != null && dayToExclude == dateToCheck.get(Calendar.DAY_OF_WEEK)) {
return true;
}
else continue;
}
return false;
}
¿Puede alguien decirme si mi lógica es correcta y si no cómo hacerlo? No estoy muy seguro de cómo se comportaría este código cuando el mes cambie durante el fin de semana.
Salida esperada para algunos casos de prueba:
- Viernes 6 p. M., Lunes 6 a.m. - debe regresar 12 horas
- Sábado a las 12:00 p.m., domingo a las 12 p.m. - se deben devolver 0 horas
- Sábado a las 12:00 p.m., lunes a las 6 a.m. - se deben devolver 6 horas
JodaTime es el camino a seguir, por lo que @KatjaChristiansen está en el camino correcto. Si necesita usar el calendario de Java, mi solución se vería así:
private static final long MILLIS_OF_WEEK = TimeUnit.DAYS.toMillis(7);
private static final long MILLIS_OF_WORKWEEK = TimeUnit.DAYS.toMillis(5);
public static int getMinsBetween(Date d1, Date d2, boolean onlyBusinessDays) {
long duration = d2.getTime() - d1.getTime();
if (onlyBusinessDays) {
Date sat = toSaturdayMidnight(d1);
long timeBeforeWeekend = Math.max(sat.getTime() - d1.getTime(), 0);
if (duration > timeBeforeWeekend) {
Date mon = toMondayMidnight(d2);
long timeAfterWeekend = Math.max(d2.getTime() - mon.getTime(), 0);
long numberOfWeekends = Math.max((duration / MILLIS_OF_WEEK) - 1, 0);
duration = numberOfWeekends * MILLIS_OF_WORKWEEK + timeBeforeWeekend + timeAfterWeekend;
}
}
return (int) TimeUnit.MILLISECONDS.toMinutes(duration);
}
private static Date toMondayMidnight(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
switch (cal.get(Calendar.DAY_OF_WEEK)) {
case Calendar.SATURDAY:
case Calendar.SUNDAY:
cal.add(Calendar.DAY_OF_MONTH, 7);
}
cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
toMidnight(cal);
return cal.getTime();
}
private static Date toSaturdayMidnight(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
toMidnight(cal);
return cal.getTime();
}
private static void toMidnight(Calendar cal) {
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
}
Estas pruebas pasan:
@Test
public void testWithinSameDay() {
assertMinsBetween(30, "2013-01-03 9:00", "2013-01-03 9:30");
}
@Test
public void testOverWeekend() {
assertMinsBetween(60, "2013-01-04 23:30", "2013-01-07 0:30");
}
@Test
public void testWeekendStart() {
assertMinsBetween(30, "2013-01-05 23:30", "2013-01-07 0:30");
}
@Test
public void testTwoWeeks() {
assertMinsBetween((int) TimeUnit.DAYS.toMinutes(10), "2013-01-08 23:30", "2013-01-22 23:30");
}
@Test
public void testTwoWeeksAndOneDay() {
assertMinsBetween((int) TimeUnit.DAYS.toMinutes(11), "2013-01-08 23:30", "2013-01-23 23:30");
}
@Test
public void testOneWeekMinusOneDay() {
assertMinsBetween((int) TimeUnit.DAYS.toMinutes(4), "2013-01-09 23:30", "2013-01-15 23:30");
}
private void assertMinsBetween(int expected, String start, String end) {
try {
assertEquals(expected, getMinsBetween(FORMAT.parse(start), FORMAT.parse(end), true));
}
catch (ParseException e) {
throw new IllegalStateException(e);
}
}
Recomiendo usar Joda-Time para cualquier cosa relacionada con la manipulación de fechas en Java, ya que viene con muchas funciones útiles para que el código sea menos complicado.
Este código usa JodaTime:
public static final List<Integer> NON_WORKING_DAYS;
static {
List<Integer> nonWorkingDays = new ArrayList<Integer>();
nonWorkingDays.add(DateTimeConstants.SATURDAY);
nonWorkingDays.add(DateTimeConstants.SUNDAY);
NON_WORKING_DAYS = Collections.unmodifiableList(nonWorkingDays);
}
public static Minutes getMinsBetween(DateTime d1, DateTime d2,
boolean onlyBusinessDays) {
BaseDateTime startDate = onlyBusinessDays && !isBusinessDay(d1) ?
new DateMidnight(d1) : d1;
BaseDateTime endDate = onlyBusinessDays && !isBusinessDay(d2) ?
new DateMidnight(d2) : d2;
Minutes minutes = Minutes.minutesBetween(startDate, endDate);
if (onlyBusinessDays) {
DateTime d = new DateTime(startDate);
while (d.isBefore(endDate)) {
if (!isBusinessDay(d)) {
Duration dayDuration = new Duration(d, d.plusDays(1));
minutes = minutes.minus(int) dayDuration.getStandardMinutes());
}
d = d.plusDays(1);
}
}
return minutes;
}
private static boolean isBusinessDay(DateTime dateToCheck) {
return !NON_WORKING_DAYS.contains(dateToCheck.dayOfWeek().get());
}
Cuando se prueba este código, ofrece los siguientes resultados:
DateTime d1 = new DateTime(2013, 1, 4, 18, 0); // a Friday, 6 pm
DateTime d2 = new DateTime(2013, 1, 7, 6, 0); // the following Monday, 6 am
Minutes minutes = getMinsBetween(d1, d2, true);
System.out.println(minutes.toStandardHours().getHours()); // outputs "12" (in hours)
d1 = new DateTime(2013, 1, 5, 12, 0); // a Saturday, 12 pm
d2 = new DateTime(2013, 1, 6, 12, 0); // the following Sunday, 12 pm
minutes = getMinsBetween(d1, d2, true);
System.out.println(minutes.toStandardHours().getHours()); // outputs "0" (in hours)
d1 = new DateTime(2013, 1, 5, 12, 0); // a Saturday, 12 pm
d2 = new DateTime(2013, 1, 7, 6, 0); // the following Monday, 6 am
minutes = getMinsBetween(d1, d2, true);
System.out.println(minutes.toStandardHours().getHours()); // outputs "6" (in hours)
Acabo de probar un caso en el que el mes cambia durante el fin de semana: desde el viernes 29 de marzo (6 p.m.) hasta el lunes 1 de abril (6 a.m.):
d1 = new DateTime(2013, 3, 29, 18, 0);
d2 = new DateTime(2013, 4, 1, 6, 0);
minutes = getMinsBetween(d1, d2, true);
System.out.println(minutes.toStandardHours().getHours());
El resultado es 12 horas, por lo que funciona para el cambio de mes.
Mi primera solución no fue manejar los horarios de verano correctamente. Tenemos que determinar la duración de cada día real al restar los minutos porque los días con un cambio en el horario de verano no serán de 24 horas:
if (!isBusinessDay(d)) {
Duration dayDuration = new Duration(d, d.plusDays(1));
minutes = minutes.minus(int) dayDuration.getStandardMinutes());
}