All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.jabb.util.time.TimeZoneUtility Maven / Gradle / Ivy

/**
 * 
 */
package net.sf.jabb.util.time;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;

/**
 * Utility for time zone related functions.
 * The shortened ids of ZoneIds are 2-character strings containing only a-z characters.
 * @author James Hu
 *
 */
public abstract class TimeZoneUtility {
    /**
     * All possible chars for representing a number as a String
     */
    final static char[] digits = {
        'a' , 'b' ,
        'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
        'i' , 'j' , 'k' , 'l' , 'm' , 'n' ,
        'o' , 'p' , 'q' , 'r' , 's' , 't' ,
        'u' , 'v' , 'w' , 'x' , 'y' , 'z'
    };

    
	static protected final Base64.Encoder base64 = Base64.getUrlEncoder().withoutPadding();
	static protected final HashFunction hashToIntFunction = Hashing.murmur3_32();
	static protected final Charset CHARSET_FOR_ENCODING = StandardCharsets.UTF_8;
	static public final int SHORTENED_ZONE_ID_MIN_LENGTH = 1;
	static public final int SHORTENED_ZONE_ID_MAX_LENGTH = 2;

	static public ZoneId UTC = ZoneId.of("UTC");
	
	static protected BiMap shortenedIdToZoneIdMapping;
	static protected BiMap indexToZoneIdMapping;
	
	static{
		Map shortenedIdMap = new HashMap<>();
		Map indexMap = new HashMap<>();
		try(BufferedReader in = new BufferedReader(new InputStreamReader(TimeZoneUtility.class.getResourceAsStream("SortedZoneIds.txt"), StandardCharsets.UTF_8))){
			String line;
			int i = 0;
			while((line = in.readLine()) != null){
				String id = line.trim();
				if (id.length() > 0 && !id.startsWith("#")){	// skip empty lines and comment lines
					if (isValidZoneId(id)){
						String shortenedId = intToAlphaString(i);
						shortenedIdMap.put(shortenedId, id);
						indexMap.put(i, id);
					}else{
						System.err.println("INFO: Unknown ZoneId: " + id);
					}
					i ++;
				}
			}
		} catch(Exception e){
			System.err.println("ERROR: Failed to read sorted ZoneIds from resource");
			e.printStackTrace();
		}
		shortenedIdToZoneIdMapping = ImmutableBiMap.copyOf(shortenedIdMap);
		indexToZoneIdMapping = ImmutableBiMap.copyOf(indexMap);
		
		if (!shortenedIdToZoneIdMapping.values().containsAll(ZoneId.getAvailableZoneIds())){
			Set newZoneIds = new HashSet<>();
			newZoneIds.addAll(ZoneId.getAvailableZoneIds());
			newZoneIds.removeAll(shortenedIdToZoneIdMapping.values());
			System.err.println("WARN: There are new time zones: " + newZoneIds);
		}
	}
	
	/**
	 * Get the mapping between the shortened id and the original id of ZoneId.
	 * Offset-based zone IDs may not be included.
	 * @return	the mapping of (shortened id - original id)
	 */
	public static BiMap getShortenedIdToZoneIdMapping() {
		return shortenedIdToZoneIdMapping;
	}
	
	/**
	 * Check to see if the shortened id is valid
	 * @param shortenedId	the shortened id to be tested
	 * @return	true if it is valid, false otherwise
	 */
	public static boolean isValidShortenedId(String shortenedId){
		return shortenedIdToZoneIdMapping.containsKey(shortenedId);
	}
	
	/**
	 * Check to see if the zone id is valid. 
	 * Please note that offset-based zone IDs are valid but they may not have corresponding shortened ids.
	 * @param zoneId	the zone id
	 * @return	true if it is valid, false otherwise
	 */
	public static boolean isValidZoneId(String zoneId){
		try{
			ZoneId.of(zoneId);
			return true;
		}catch(Exception e){
			return false;
		}
	}

	/**
	 * Get the 2-character shortened id of the zone
	 * @param zoneId	id of the time zone
	 * @return	the shortened id, or null if zoneId is not valid
	 */
	static public String toShortenedId(String zoneId){
		return shortenedIdToZoneIdMapping.inverse().get(zoneId);
	}
	
	/**
	 * Get the 2-character shortened id of the zone
	 * @param zoneId	time zone
	 * @return	the shortened id
	 */
	static public String toShortenedId(ZoneId zoneId){
		return shortenedIdToZoneIdMapping.inverse().get(zoneId.getId());
	}
	
	/**
	 * Get the ZoneId corresponding to the shortened id
	 * @param id	the shortened id, or the original id
	 * @return	the ZoneId, or null if not found
	 */
	static public ZoneId toZoneId(String id){
		String zoneId = shortenedIdToZoneIdMapping.get(id);
		if (zoneId != null){
			return ZoneId.of(zoneId);
		}
		try{
			return ZoneId.of(id);
		}catch(Exception e){
			return null;
		}
	}
	
	/**
	 * Get the id of the ZoneId corresponding to the shortened id
	 * @param shortenedId	the shortened id
	 * @return	the id of the ZoneId, or null if not found
	 */
	static public String toZoneIdId(String shortenedId){
		return shortenedIdToZoneIdMapping.get(shortenedId);
	}
	
	/**
	 * Get the index of the zone
	 * @param zoneId	id of the time zone
	 * @return	the index, or null if zoneId is not valid
	 */
	static public Integer toIndex(String zoneId){
		return indexToZoneIdMapping.inverse().get(zoneId);
	}
	
	/**
	 * Get the index of the zone
	 * @param zoneId	time zone
	 * @return	the index
	 */
	static public Integer toIndex(ZoneId zoneId){
		return indexToZoneIdMapping.inverse().get(zoneId.getId());
	}
	
	/**
	 * Get the ZoneId corresponding to the index
	 * @param index	the index
	 * @return	the ZoneId, or null if not found
	 */
	static public ZoneId toZoneId(int index){
		String zoneId = indexToZoneIdMapping.get(index);
		return zoneId == null ? null : ZoneId.of(zoneId);
	}
	
	/**
	 * Get the id of the ZoneId corresponding to the index
	 * @param index	the index
	 * @return	the id of the ZoneId, or null if not found
	 */
	static public String toZoneIdId(int index){
		return indexToZoneIdMapping.get(index);
	}
	
	static protected List getSortedZoneIds(){
		Set all = ZoneId.getAvailableZoneIds();
		@SuppressWarnings("unchecked")
		Predicate[] filters = new Predicate[] {
			x->((String)x).startsWith("UTC"),
			x->((String)x).equals("GMT"),
			x->((String)x).equals("Etc/GMT"),
			x->((String)x).startsWith("Etc/GMT-") && ((String)x).length() <= 9,
			x->((String)x).equals("Etc/GMT-10") || ((String)x).equals("Etc/GMT-11"),
			x->((String)x).startsWith("Etc/GMT+") && ((String)x).length() <= 9,
			x->((String)x).startsWith("Etc/GMT+") && ((String)x).length() > 9,
			x->true
		};
		Predicate lastFilter= filters[filters.length - 1];
		for (int i = 0; i < filters.length - 1; i ++){
			lastFilter = lastFilter.and(filters[i].negate());
		}
		filters[filters.length - 1] = lastFilter;
		
		Stream result = Stream.empty();
		for (Predicate f: filters){
			result = Stream.concat(result, all.stream().filter(f).sorted());
		}

		return result.collect(Collectors.toList());
	}
	
	/**
	 * Convert a integer value to a radix-26 encoded string.
	 * The encoding is not standard, it uses only a-z characters
	 * @param i		the integer
	 * @return	the encoding result by the modified radix-36
	 */
	static protected String intToAlphaString(int i){
		int radix = 26;
        char buf[] = new char[33];
        boolean negative = (i < 0);
        int charPos = 32;

        if (!negative) {
            i = -i;
        }

        while (i <= -radix) {
            buf[charPos--] = digits[-(i % radix)];
            i = i / radix;
        }
        buf[charPos] = digits[-i];

        if (negative) {
            buf[--charPos] = '-';
        }

        return new String(buf, charPos, (33 - charPos));
	}
	
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy