DaylightSavingUtils.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2024 Link.it srl (https://link.it). 
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3, as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package org.openspcoop2.utils.date;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneRules;

/**     
 * DaylightSavingUtils
 *
 * @author Poli Andrea (poli@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public class DaylightSavingUtils {
	
	/**
	 * Ora Solare (CET - Central European Time): L'ora solare è il tempo standard utilizzato durante l'inverno. In Europa, è UTC+1, il che significa che è un'ora avanti rispetto al Coordinated Universal Time (UTC). L'ora solare è usata dal tardo autunno all'inizio della primavera, solitamente da fine ottobre a fine marzo.

	   Ora Legale (CEST - Central European Summer Time): L'ora legale viene utilizzata durante l'estate per risparmiare energia, spostando le lancette avanti di un'ora. In Europa, è UTC+2, ovvero due ore avanti rispetto a UTC. L'ora legale è in vigore da fine marzo a fine ottobre.

	   Passaggio da Ora Legale a Ora Solare: In autunno (solitamente l'ultima domenica di ottobre), l'orologio viene spostato indietro di un'ora alle 03:00 AM CEST, tornando alle 02:00 AM CET (ora solare).
	   Passaggio da Ora Solare a Ora Legale: In primavera (solitamente l'ultima domenica di marzo), l'orologio viene spostato avanti di un'ora alle 02:00 AM CET, diventando le 03:00 AM CEST (ora legale).
	 */
	
	private DaylightSavingUtils() {}

    // Formato per il parsing delle date con offset
	private static final String OFFSET_FORMAT = "yyyy-MM-dd HH:mm:ssXXX";
	public static String getOffsetFormat() {
		return OFFSET_FORMAT;
	}
    private static final DateTimeFormatter OFFSET_FORMATTER = DateTimeFormatter.ofPattern(OFFSET_FORMAT);
    public static DateTimeFormatter getOffsetFormatter() {
		return OFFSET_FORMATTER;
	}

	// Formato per il parsing delle date senza offset
    private static final String BASIC_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static String getBasicFormat() {
		return BASIC_FORMAT;
	}
    private static final DateTimeFormatter BASIC_FORMATTER = DateTimeFormatter.ofPattern(BASIC_FORMAT);
    public static DateTimeFormatter getBasicFormatter() {
		return BASIC_FORMATTER;
	}

	// Zona di riferimento, ad esempio Europe/Rome
    /**private static final ZoneId ZONE_ID = ZoneId.of("Europe/Rome");*/
    private static final ZoneId ZONE_ID_DEFAULT = ZoneId.systemDefault();
    public static ZoneId getZoneIdDefault() {
		return ZONE_ID_DEFAULT;
	}
    
	/**
     * Calcola quanti minuti mancano al prossimo cambio di ora legale o solare
     * per una data con offset di fuso orario.
     *
     * @param inputDate La data in formato "yyyy-MM-dd HH:mm:ss+01"
     * @return Minuti mancanti al cambio di ora
     */
    public static long minutesUntilNextTransition(String inputDate) {
    	return minutesUntilNextTransition(inputDate, ZONE_ID_DEFAULT);
    }
    public static long minutesUntilNextTransition(String inputDate, ZoneId zoneId) {
    	return minutesUntilNextTransition(inputDate, zoneId, null, null);
    }
    public static long minutesUntilNextTransition(String inputDate, String format) {
    	return minutesUntilNextTransition(inputDate, ZONE_ID_DEFAULT, format);
    }
    public static long minutesUntilNextTransition(String inputDate, ZoneId zoneId, String format) {
    	return minutesUntilNextTransition(inputDate, zoneId, format, null);
    }
    public static long minutesUntilNextTransition(String inputDate, DateTimeFormatter formatter) {
    	return minutesUntilNextTransition(inputDate, ZONE_ID_DEFAULT, formatter);
    }
    public static long minutesUntilNextTransition(String inputDate, ZoneId zoneId, DateTimeFormatter formatter) {
    	return minutesUntilNextTransition(inputDate, zoneId, null, formatter);
    }
    private static long minutesUntilNextTransition(String inputDate, ZoneId zoneId, String format, DateTimeFormatter formatter) {
        OffsetDateTime offsetDateTime = formatter!=null ? parseOffsetDateTime(inputDate, formatter) : parseOffsetDateTime(inputDate, format);
        ZonedDateTime zonedDateTime = offsetDateTime.atZoneSameInstant(zoneId);

        ZoneRules zoneRules = zoneId.getRules();
        ZoneOffsetTransition nextTransition = zoneRules.nextTransition(zonedDateTime.toInstant());

        if (nextTransition != null) {
        	ZonedDateTime transitionDateTime = ZonedDateTime.ofInstant(nextTransition.getDateTimeBefore(), nextTransition.getOffsetBefore(), zoneId);
            return Duration.between(zonedDateTime, transitionDateTime).toMinutes();
        }
        return -1; // Nessuna transizione trovata
    }

    /**
     * Calcola quanti minuti mancano al prossimo cambio di ora legale o solare
     * per una data senza offset di fuso orario.
     *
     * @param inputDate La data in formato "yyyy-MM-dd HH:mm:ss"
     * @return Minuti mancanti al cambio di ora
     */
    public static long minutesUntilNextTransitionWithoutOffset(String inputDate) {
    	return minutesUntilNextTransitionWithoutOffset(inputDate, ZONE_ID_DEFAULT);
    }
    public static long minutesUntilNextTransitionWithoutOffset(String inputDate, ZoneId zoneId) {
    	return minutesUntilNextTransitionWithoutOffset(inputDate, zoneId, null, null);
    }
    public static long minutesUntilNextTransitionWithoutOffset(String inputDate, String format) {
    	return minutesUntilNextTransitionWithoutOffset(inputDate, ZONE_ID_DEFAULT, format);
    }
    public static long minutesUntilNextTransitionWithoutOffset(String inputDate, ZoneId zoneId, String format) {
    	return minutesUntilNextTransitionWithoutOffset(inputDate, zoneId, format, null);
    }
    public static long minutesUntilNextTransitionWithoutOffset(String inputDate, DateTimeFormatter formatter) {
    	return minutesUntilNextTransitionWithoutOffset(inputDate, ZONE_ID_DEFAULT, formatter);
    }
    public static long minutesUntilNextTransitionWithoutOffset(String inputDate, ZoneId zoneId, DateTimeFormatter formatter) {
    	return minutesUntilNextTransitionWithoutOffset(inputDate, zoneId, null, formatter);
    }
    private static long minutesUntilNextTransitionWithoutOffset(String inputDate, ZoneId zoneId, String format, DateTimeFormatter formatter) {
        LocalDateTime localDateTime = formatter!=null ? parseLocalDateTime(inputDate, formatter) : parseLocalDateTime(inputDate, format);
        ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);

        ZoneRules zoneRules = zoneId.getRules();
        ZoneOffsetTransition nextTransition = zoneRules.nextTransition(zonedDateTime.toInstant());

        if (nextTransition != null) {
        	ZonedDateTime transitionDateTime = ZonedDateTime.ofInstant(nextTransition.getDateTimeBefore(), nextTransition.getOffsetBefore(), zoneId);
            return Duration.between(zonedDateTime, transitionDateTime).toMinutes();
        }
        return -1; // Nessuna transizione trovata
    }
    
    
    /**
     * Determina se una certa data si trova in ora legale o ora solare.
     *
     * @param inputDate La data in formato "yyyy-MM-dd HH:mm:ss+01"
     * @return "ora legale" o "ora solare" a seconda della data
     */
    public static TimeType getTimeType(String inputDate) {
    	return getTimeType(inputDate, ZONE_ID_DEFAULT);
    }
    public static TimeType getTimeType(String inputDate, ZoneId zoneId) {
    	return getTimeType(inputDate, zoneId, null, null);
    }
    public static TimeType getTimeType(String inputDate, String format) {
    	return getTimeType(inputDate, ZONE_ID_DEFAULT, format);
    }
    public static TimeType getTimeType(String inputDate, ZoneId zoneId, String format) {
    	return getTimeType(inputDate, zoneId, format, null);
    }
    public static TimeType getTimeType(String inputDate, DateTimeFormatter formatter) {
    	return getTimeType(inputDate, ZONE_ID_DEFAULT, formatter);
    }
    public static TimeType getTimeType(String inputDate, ZoneId zoneId, DateTimeFormatter formatter) {
    	return getTimeType(inputDate, zoneId, null, formatter);
    }
    private static TimeType getTimeType(String inputDate, ZoneId zoneId, String format, DateTimeFormatter formatter) {
        OffsetDateTime offsetDateTime = formatter!=null ? parseOffsetDateTime(inputDate, formatter) : parseOffsetDateTime(inputDate, format);
        ZonedDateTime zonedDateTime = offsetDateTime.atZoneSameInstant(zoneId);

        boolean isDaylightSavings = zonedDateTime.getZone().getRules().isDaylightSavings(zonedDateTime.toInstant());
        return isDaylightSavings ? TimeType.DAYLIGHT_SAVING_TIME : TimeType.STANDARD_TIME;
    }

    /**
     * Converte una data in formato "yyyy-MM-dd HH:mm:ss+01" in un ZonedDateTime
     * per il fuso orario specificato (es. Europe/Rome).
     *
     * @param inputDate La data in formato "yyyy-MM-dd HH:mm:ss+01"
     * @return ZonedDateTime corrispondente
     */
    public static ZonedDateTime convertToZonedDateTime(String inputDate) {
    	return convertToZonedDateTime(inputDate, ZONE_ID_DEFAULT);
    }
    public static ZonedDateTime convertToZonedDateTime(String inputDate, ZoneId zoneId) {
    	return convertToZonedDateTime(inputDate, zoneId, null, null);
    }
    public static ZonedDateTime convertToZonedDateTime(String inputDate, String format) {
    	return convertToZonedDateTime(inputDate, ZONE_ID_DEFAULT, format);
    }
    public static ZonedDateTime convertToZonedDateTime(String inputDate, ZoneId zoneId, String format) {
    	return convertToZonedDateTime(inputDate, zoneId, format, null);
    }
    public static ZonedDateTime convertToZonedDateTime(String inputDate, DateTimeFormatter formatter) {
    	return convertToZonedDateTime(inputDate, ZONE_ID_DEFAULT, formatter);
    }
    public static ZonedDateTime convertToZonedDateTime(String inputDate, ZoneId zoneId, DateTimeFormatter formatter) {
    	return convertToZonedDateTime(inputDate, zoneId, null, formatter);
    }
    private static ZonedDateTime convertToZonedDateTime(String inputDate, ZoneId zoneId, String format, DateTimeFormatter formatter) {
        OffsetDateTime offsetDateTime = formatter!=null ? parseOffsetDateTime(inputDate, formatter) : parseOffsetDateTime(inputDate, format);
        return offsetDateTime.atZoneSameInstant(zoneId);
    }

    
    /**
     * Determina se una data è nel giorno in cui avviene il cambio tra ora legale e ora solare (o viceversa).
     *
     * @param inputDate La data in formato "yyyy-MM-dd HH:mm:ss+01:00"
     * @return true se la data è nel giorno del cambio di ora, false altrimenti
     */
    public static TimeTransitionType getTimeChangePendingToday(String inputDate) {
    	return getTimeChangePendingToday(inputDate, ZONE_ID_DEFAULT);
    }
    public static TimeTransitionType getTimeChangePendingToday(String inputDate, ZoneId zoneId) {
    	return getTimeChangePendingToday(inputDate, zoneId, null, null);
    }
    public static TimeTransitionType getTimeChangePendingToday(String inputDate, String format) {
    	return getTimeChangePendingToday(inputDate, ZONE_ID_DEFAULT, format);
    }
    public static TimeTransitionType getTimeChangePendingToday(String inputDate, ZoneId zoneId, String format) {
    	return getTimeChangePendingToday(inputDate, zoneId, format, null);
    }
    public static TimeTransitionType getTimeChangePendingToday(String inputDate, DateTimeFormatter formatter) {
    	return getTimeChangePendingToday(inputDate, ZONE_ID_DEFAULT, formatter);
    }
    public static TimeTransitionType getTimeChangePendingToday(String inputDate, ZoneId zoneId, DateTimeFormatter formatter) {
    	return getTimeChangePendingToday(inputDate, zoneId, null, formatter);
    }
    private static TimeTransitionType getTimeChangePendingToday(String inputDate, ZoneId zoneId, String format, DateTimeFormatter formatter) {
        // Parse della stringa in OffsetDateTime
        OffsetDateTime dateTime = formatter!=null ? parseOffsetDateTime(inputDate, formatter) : parseOffsetDateTime(inputDate, format);

        // Ottieni il fuso orario dell'ambiente (ad esempio Europe/Rome)
         ZoneRules zoneRules = zoneId.getRules();

        // Ottieni la transizione successiva
        ZoneOffsetTransition nextTransition = zoneRules.nextTransition(dateTime.toInstant());

        if (nextTransition != null) {
            // Ottieni la data e l'ora del giorno della transizione
            LocalDate transitionDate = nextTransition.getDateTimeBefore().toLocalDate();

            // Verifica se la data fornita è nello stesso giorno della transizione
            if( dateTime.toLocalDate().equals(transitionDate) ) {
        	     // Ottieni gli offset prima e dopo la transizione
                ZoneOffset offsetBefore = nextTransition.getOffsetBefore();
                ZoneOffset offsetAfter = nextTransition.getOffsetAfter();

                // Determina il tipo di transizione
                if (offsetBefore.getTotalSeconds() > offsetAfter.getTotalSeconds()) {
                	return TimeTransitionType.FROM_DAYLIGHT_SAVING_TO_STANDARD_TIME;  // Ora legale a ora solare
                } else {
                    return TimeTransitionType.FROM_STANDARD_TO_DAYLIGHT_SAVING_TIME;  // Ora solare a ora legale
                }

            }
        }
        
        return null;  // Nessuna transizione trovata
    }
    
    
    /**
     * Determina se una data è nel giorno in cui avviene il cambio tra ora legale e ora solare (o viceversa) per una data senza offset di fuso orario.
     *
     * @param inputDate La data in formato "yyyy-MM-dd HH:mm:ss"
     * @return true se la data è nel giorno del cambio di ora, false altrimenti
     */
    public static TimeTransitionType getTimeChangePendingTodayWithoutOffset(String inputDate) {
    	return getTimeChangePendingTodayWithoutOffset(inputDate, ZONE_ID_DEFAULT);
    }
    public static TimeTransitionType getTimeChangePendingTodayWithoutOffset(String inputDate, ZoneId zoneId) {
    	return getTimeChangePendingTodayWithoutOffset(inputDate, zoneId, null, null);
    }
    public static TimeTransitionType getTimeChangePendingTodayWithoutOffset(String inputDate, String format) {
    	return getTimeChangePendingTodayWithoutOffset(inputDate, ZONE_ID_DEFAULT, format);
    }
    public static TimeTransitionType getTimeChangePendingTodayWithoutOffset(String inputDate, ZoneId zoneId, String format) {
    	return getTimeChangePendingTodayWithoutOffset(inputDate, zoneId, format, null);
    }
    public static TimeTransitionType getTimeChangePendingTodayWithoutOffset(String inputDate, DateTimeFormatter formatter) {
    	return getTimeChangePendingTodayWithoutOffset(inputDate, ZONE_ID_DEFAULT, formatter);
    }
    public static TimeTransitionType getTimeChangePendingTodayWithoutOffset(String inputDate, ZoneId zoneId, DateTimeFormatter formatter) {
    	return getTimeChangePendingTodayWithoutOffset(inputDate, zoneId, null, formatter);
    }
    private static TimeTransitionType getTimeChangePendingTodayWithoutOffset(String inputDate, ZoneId zoneId, String format, DateTimeFormatter formatter) {
    	
    	LocalDateTime localDateTime = formatter!=null ? parseLocalDateTime(inputDate, formatter) : parseLocalDateTime(inputDate, format);
        ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);

        ZoneRules zoneRules = zoneId.getRules();
        ZoneOffsetTransition nextTransition = zoneRules.nextTransition(zonedDateTime.toInstant());

        if (nextTransition != null) {
        	ZonedDateTime transitionDateTime = ZonedDateTime.ofInstant(nextTransition.getDateTimeBefore(), nextTransition.getOffsetBefore(), zoneId);
        	
        	  // Verifica se la data fornita è nello stesso giorno della transizione
            if( localDateTime.toLocalDate().equals(transitionDateTime.toLocalDate()) ) {
        	     // Ottieni gli offset prima e dopo la transizione
                ZoneOffset offsetBefore = nextTransition.getOffsetBefore();
                ZoneOffset offsetAfter = nextTransition.getOffsetAfter();

                // Determina il tipo di transizione
                if (offsetBefore.getTotalSeconds() > offsetAfter.getTotalSeconds()) {
                	return TimeTransitionType.FROM_DAYLIGHT_SAVING_TO_STANDARD_TIME;  // Ora legale a ora solare
                } else {
                    return TimeTransitionType.FROM_STANDARD_TO_DAYLIGHT_SAVING_TIME;  // Ora solare a ora legale
                }

            }
        }
    	
        return null;  // Nessuna transizione trovata
    }
    
    
    
    /** Parser */
    
    public static LocalDateTime parseLocalDateTime(String inputDate) {
    	return parseLocalDateTime(inputDate, BASIC_FORMATTER);
    }
    public static LocalDateTime parseLocalDateTime(String inputDate, String format) {
    	if(format==null) {
    		return parseLocalDateTime(inputDate);
    	}
    	return parseLocalDateTime(inputDate, DateTimeFormatter.ofPattern(format));
    }
    public static LocalDateTime parseLocalDateTime(String inputDate, DateTimeFormatter formatter) {
    	if(formatter==null) {
    		return parseLocalDateTime(inputDate);
    	}
    	return LocalDateTime.parse(inputDate, formatter);
    }
    
    public static OffsetDateTime parseOffsetDateTime(String inputDate) {
    	return parseOffsetDateTime(inputDate, OFFSET_FORMATTER);
    }
    public static OffsetDateTime parseOffsetDateTime(String inputDate, String format) {
    	if(format==null) {
    		return parseOffsetDateTime(inputDate);
    	}
    	return parseOffsetDateTime(inputDate, DateTimeFormatter.ofPattern(format));
    }
    public static OffsetDateTime parseOffsetDateTime(String inputDate, DateTimeFormatter formatter) {
    	if(formatter==null) {
    		return parseOffsetDateTime(inputDate);
    	}
    	if (inputDate.matches(".*[+-]\\d{2}$")) {
            inputDate += ":00";
        }
    	return OffsetDateTime.parse(inputDate, formatter);
    }
}