Skip to content

Commit

Permalink
Generate a VTimezone when one could not be found.
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgrigore committed Jan 11, 2017
1 parent a1d95d7 commit 155e8be
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 5 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Expand Up @@ -22,8 +22,8 @@ apply plugin: 'signing'
apply plugin: 'pl.allegro.tech.build.axion-release'
apply from: 'gradle/dist.gradle'

sourceCompatibility = 1.6
targetCompatibility = 1.6
sourceCompatibility = 1.8
targetCompatibility = 1.8

/*
githubPages {
Expand Down
215 changes: 212 additions & 3 deletions src/main/java/net/fortuna/ical4j/model/TimeZoneRegistryImpl.java
Expand Up @@ -33,7 +33,16 @@

import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.component.Daylight;
import net.fortuna.ical4j.model.component.Observance;
import net.fortuna.ical4j.model.component.Standard;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.property.DtStart;
import net.fortuna.ical4j.model.property.RDate;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.TzId;
import net.fortuna.ical4j.model.property.TzOffsetFrom;
import net.fortuna.ical4j.model.property.TzOffsetTo;
import net.fortuna.ical4j.model.property.TzUrl;
import net.fortuna.ical4j.util.CompatibilityHints;
import net.fortuna.ical4j.util.Configurator;
Expand All @@ -47,8 +56,25 @@
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.time.DayOfWeek;
import java.time.Period;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.TemporalAdjusters;
import java.time.zone.ZoneOffsetTransition;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -229,8 +255,9 @@ public final TimeZone getTimeZone(final String id) {

/**
* Loads an existing VTimeZone from the classpath corresponding to the specified Java timezone.
* @throws ParseException
*/
private VTimeZone loadVTimeZone(final String id) throws IOException, ParserException {
private VTimeZone loadVTimeZone(final String id) throws IOException, ParserException, ParseException {
final URL resource = ResourceLoader.getResource(resourcePrefix + id + ".ics");
if (resource != null) {
final CalendarBuilder builder = new CalendarBuilder();
Expand All @@ -242,7 +269,7 @@ private VTimeZone loadVTimeZone(final String id) throws IOException, ParserExcep
}
return vTimeZone;
}
return null;
return generateTimezoneForId(id);
}

/**
Expand Down Expand Up @@ -286,4 +313,186 @@ private VTimeZone updateDefinition(VTimeZone vTimeZone) {
}
return vTimeZone;
}
}



private static final Set<String> TIMEZONE_DEFINITIONS = new HashSet<>();

private static final String DATE_TIME_TPL = "%1$tY%1$tm%1$tdT%1$tH%1$tM%1$tS";

private static final String RRULE_TPL = "FREQ=YEARLY;BYMONTH=%d;BYDAY=%d%s";

private static final Standard NO_TRANSITIONS;

static {
for(String timezoneId : TimeZone.getAvailableIDs() ){
TIMEZONE_DEFINITIONS.add(timezoneId);
}
NO_TRANSITIONS = new Standard();

TzOffsetFrom offsetFrom = new TzOffsetFrom(new UtcOffset(0));
TzOffsetTo offsetTo = new TzOffsetTo(new UtcOffset(0));
NO_TRANSITIONS.getProperties().add(offsetFrom);
NO_TRANSITIONS.getProperties().add(offsetTo);

DtStart start = new DtStart();
start.setDate(new DateTime(0L));
NO_TRANSITIONS.getProperties().add(start);

}

private static VTimeZone generateTimezoneForId(String timezoneId) throws ParseException {
if(!TIMEZONE_DEFINITIONS.contains(timezoneId)){
return null;
}
java.util.TimeZone javaTz = java.util.TimeZone.getTimeZone(timezoneId);

ZoneId zoneId = javaTz.toZoneId();

int rawTimeZoneOffsetInSeconds = javaTz.getRawOffset() / 1000;

VTimeZone timezone = new VTimeZone();

timezone.getProperties().add(new TzId(timezoneId));

addTransitions(zoneId, timezone, rawTimeZoneOffsetInSeconds);

addTransitionRules(zoneId, rawTimeZoneOffsetInSeconds, timezone);

if(timezone.getObservances() == null || timezone.getObservances().isEmpty()){
timezone.getObservances().add(NO_TRANSITIONS);
}

return timezone;
}

private static void addTransitionRules(ZoneId zoneId, int rawTimeZoneOffsetInSeconds, VTimeZone result) {
LocalDateTime startDate = zoneId.getRules().getTransitions()
.stream()
.map(z->{return z.getDateTimeBefore();})
.reduce((d1, d2)->{
return d1.compareTo(d2) > 0 ? d1 : d2;
}).orElse(LocalDateTime.now(zoneId));

zoneId.getRules().getTransitionRules().stream().forEach(transitionRule ->{
int transitionRuleMonthValue = transitionRule.getMonth().getValue();
DayOfWeek transitionRuleDayOfWeek = transitionRule.getDayOfWeek();
LocalDateTime ldt = LocalDateTime.now(zoneId)
.with(TemporalAdjusters.firstInMonth(transitionRuleDayOfWeek))
.withMonth(transitionRuleMonthValue)
.with(transitionRule.getLocalTime());
Month month = ldt.getMonth();

TreeSet<Integer> allDaysOfWeek = new TreeSet<>();

do{
allDaysOfWeek.add(ldt.getDayOfMonth());
}while((ldt = ldt.plus(Period.ofWeeks(1))).getMonth() == month);

Integer dayOfMonth = Optional.ofNullable(allDaysOfWeek.ceiling(transitionRule.getDayOfMonthIndicator())).orElseGet(()->{return allDaysOfWeek.last();});

int weekdayIndexInMonth = 0;
for(Iterator<Integer> it = allDaysOfWeek.iterator(); it.hasNext() && it.next() != dayOfMonth;){
weekdayIndexInMonth++;
}

weekdayIndexInMonth = weekdayIndexInMonth >= 3 ? weekdayIndexInMonth - allDaysOfWeek.size() : weekdayIndexInMonth;

String rruleTemplate = RRULE_TPL;
String rruleText = String.format(rruleTemplate,transitionRuleMonthValue, weekdayIndexInMonth, transitionRuleDayOfWeek.name().substring(0, 2));

try {
TzOffsetFrom offsetFrom = new TzOffsetFrom(new UtcOffset(transitionRule.getOffsetBefore().getTotalSeconds() * 1000L));
TzOffsetTo offsetTo = new TzOffsetTo(new UtcOffset(transitionRule.getOffsetAfter().getTotalSeconds() * 1000L));
RRule rrule = new RRule(rruleText);

Observance observance = (transitionRule.getOffsetAfter().getTotalSeconds() > rawTimeZoneOffsetInSeconds) ? new Daylight() : new Standard();

observance.getProperties().add(offsetFrom);
observance.getProperties().add(offsetTo);
observance.getProperties().add(rrule);
observance.getProperties().add(new DtStart(String.format(DATE_TIME_TPL, startDate.withMonth(transitionRule.getMonth().getValue())
.withDayOfMonth(transitionRule.getDayOfMonthIndicator())
.with(transitionRule.getDayOfWeek()))));

result.getObservances().add(observance);

} catch (Exception e) {
throw new RuntimeException(e);
}
});
}

private static void addTransitions(ZoneId zoneId, VTimeZone result, int rawTimeZoneOffsetInSeconds) throws ParseException {
Map<ZoneOffsetKey, Set<ZoneOffsetTransition>> zoneTransitionsByOffsets = new HashMap<>();

for(ZoneOffsetTransition zoneTransitionRule : zoneId.getRules().getTransitions()){
ZoneOffsetKey offfsetKey = ZoneOffsetKey.of(zoneTransitionRule.getOffsetBefore(), zoneTransitionRule.getOffsetAfter());

Set<ZoneOffsetTransition> transitionRulesForOffset = zoneTransitionsByOffsets.get(offfsetKey);
if(transitionRulesForOffset == null){
transitionRulesForOffset = new HashSet<>(1);
zoneTransitionsByOffsets.put(offfsetKey, transitionRulesForOffset);
}
transitionRulesForOffset.add(zoneTransitionRule);
}


for(Map.Entry<ZoneOffsetKey, Set<ZoneOffsetTransition>> e : zoneTransitionsByOffsets.entrySet()){

Observance observance = (e.getKey().offsetAfter.getTotalSeconds() > rawTimeZoneOffsetInSeconds) ? new Daylight() : new Standard();

LocalDateTime start = Collections.min(e.getValue()).getDateTimeBefore();

DtStart dtStart = new DtStart(String.format(DATE_TIME_TPL, start));
TzOffsetFrom offsetFrom = new TzOffsetFrom(new UtcOffset(e.getKey().offsetBefore.getTotalSeconds() * 1000L));
TzOffsetTo offsetTo = new TzOffsetTo(new UtcOffset(e.getKey().offsetAfter.getTotalSeconds() * 1000L));

observance.getProperties().add(dtStart);
observance.getProperties().add(offsetFrom);
observance.getProperties().add(offsetTo);

for(ZoneOffsetTransition transition : e.getValue()){
RDate rDate = new RDate(new ParameterList(), String.format(DATE_TIME_TPL, transition.getDateTimeBefore()));
observance.getProperties().add(rDate);
}
result.getObservances().add(observance);
}
}

private static class ZoneOffsetKey{
private final ZoneOffset offsetBefore;
private final ZoneOffset offsetAfter;

private ZoneOffsetKey(ZoneOffset offsetBefore, ZoneOffset offsetAfter){
this.offsetBefore = offsetBefore;
this.offsetAfter = offsetAfter;
}

@Override
public boolean equals(Object obj) {
if(obj == this){
return true;
}
if(!(obj instanceof ZoneOffsetKey)){
return false;
}
ZoneOffsetKey otherZoneOffsetKey = (ZoneOffsetKey)obj;
return Objects.equals(this.offsetBefore, otherZoneOffsetKey.offsetBefore) && Objects.equals(this.offsetAfter, otherZoneOffsetKey.offsetAfter);
}

@Override
public int hashCode() {
int result = 31;
result = result * (this.offsetBefore == null ? 1 : this.offsetBefore.hashCode());
result = result * (this.offsetAfter == null ? 1 : this.offsetAfter.hashCode());

return result;
}

static ZoneOffsetKey of (ZoneOffset offsetBefore, ZoneOffset offsetAfter){
return new ZoneOffsetKey(offsetBefore, offsetAfter);
}
}

}

0 comments on commit 155e8be

Please sign in to comment.