You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@brooklyn.apache.org by ahgittin <gi...@git.apache.org> on 2015/06/08 02:00:54 UTC

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

GitHub user ahgittin opened a pull request:

    https://github.com/apache/incubator-brooklyn/pull/682

    Date parsing and type coercion fixes

    

You can merge this pull request into a Git repository by running:

    $ git pull https://github.com/ahgittin/incubator-brooklyn misc-fixes

Alternatively you can review and apply these changes as the patch at:

    https://github.com/apache/incubator-brooklyn/pull/682.patch

To close this pull request, make a commit to your master/trunk branch
with (at least) the following in the commit message:

    This closes #682
    
----
commit 9fe4c7c413e6aeba6ec01cf2e10427d2829b8cb2
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Date:   2015-06-04T15:53:17Z

    clean up time API and support date parsing
    
    also a lot of help for time zones, because java seems bad with this,
    and making time methods consistent and less ambiguous

commit fc769e61b0e0daab02a11ee1c319b7be0f486520
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Date:   2015-06-04T15:53:41Z

    better REST coercion and coerce String->Date
    
    wrt REST this improves how deferred suppliers are evaluated,
    both timing and in entity's execution context

----


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by alasdairhodge <gi...@git.apache.org>.
Github user alasdairhodge commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-109936326
  
    I quite like the fluent methods for constructing regexes, although as you mention they work best under Java 7 with named group support; can we pull this into brooklyn.utils.common?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by sjcorbett <gi...@git.apache.org>.
Github user sjcorbett commented on a diff in the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#discussion_r32301812
  
    --- Diff: utils/common/src/main/java/brooklyn/util/time/Time.java ---
    @@ -443,36 +500,466 @@ else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreC
             }
         }
     
    +    public static Calendar newCalendarFromMillisSinceEpochUtc(long timestamp) {
    +        GregorianCalendar cal = new GregorianCalendar();
    +        cal.setTimeInMillis(timestamp);
    +        return cal;
    +    }
    +
    +    public static Calendar newCalendarFromDate(Date date) {
    +        return newCalendarFromMillisSinceEpochUtc(date.getTime());
    +    }
    +    
    +    /** As {@link #parseCalendar(String)} but returning a {@link Date},
    +     * (i.e. a record where the time zone has been applied and forgotten). */
    +    public static Date parseDate(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get().getTime();
    +    }
    +
    +    /** Parses dates from string, accepting many formats including ISO-8601 and http://yaml.org/type/timestamp.html, 
    +     * e.g. 2015-06-15 16:00:00 +0000.
    +     * <p>
    +     * Millis since epoch (1970) is also supported to represent the epoch (0) or dates in this millenium,
    +     * but to prevent ambiguity of e.g. "20150615", any other dates prior to the year 2001 are not accepted.
    +     * (However if a type Long is supplied, e.g. from a YAML parse, it will always be treated as millis since epoch.) 
    +     * <p>
    +     * Other formats including locale-specific variants, e.g. recognising month names,
    +     * are supported but this may vary from platform to platform and may change between versions. */
    +    public static Calendar parseCalendar(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get();
    +    }
    +    
    +    /** as {@link #parseCalendar(String)} but returning a {@link Maybe} rather than throwing or returning null */
    +    public static Maybe<Calendar> parseCalendarMaybe(String input) {
    +        if (input==null) return Maybe.absent("value is null");
    +        input = input.trim();
    +        Maybe<Calendar> result;
    +
    +        result = parseCalendarUtc(input);
    +        if (result.isPresent()) return result;
    +
    +        result = parseCalendarSimpleFlexibleFormatParser(input);
    +        if (result.isPresent()) return result;
    +        // return the error from this method
    +        Maybe<Calendar> returnResult = result;
    +
    +//        // see natty method comments below
    +//        Maybe<Date> result = parseDateNatty(input);
    +//        if (result.isPresent()) return result;
    +
    +        result = parseCalendarFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
    +        if (result.isPresent()) return result;
    +        result = parseCalendarDefaultParse(input);
    +        if (result.isPresent()) return result;
    +
    +        return returnResult;
    +    }
    +
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarDefaultParse(String input) {
    +        try {
    +            long ms = Date.parse(input);
    +            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
    +                // accept default date parse for this century and next
    +                GregorianCalendar c = new GregorianCalendar();
    +                c.setTimeInMillis(ms);
    +                return Maybe.of((Calendar)c);
    +            }
    +        } catch (Exception e) {
    +            Exceptions.propagateIfFatal(e);
    +        }
    +        return Maybe.absent();
    +    }
    +
    +    private static Maybe<Calendar> parseCalendarUtc(String input) {
    +        input = input.trim();
    +        if (input.matches("\\d+")) {
    +            if ("0".equals(input)) {
    +                // accept 0 as epoch UTC
    +                return Maybe.of(newCalendarFromMillisSinceEpochUtc(0));
    +            }
    +            Maybe<Calendar> result = Maybe.of(newCalendarFromMillisSinceEpochUtc(Long.parseLong(input)));
    +            if (result.isPresent()) {
    +                int year = result.get().get(Calendar.YEAR);
    +                if (year >= 2000 && year < 2200) {
    +                    // only applicable for dates in this century
    +                    return result;
    +                } else {
    +                    return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
    +                }
    +            }
    +        }
    +        return Maybe.absent("not long millis since epoch UTC");
    +    }
    +
    +    private final static String DIGIT = "\\d";
    +    private final static String LETTER = "\\p{L}";
    +    private final static String COMMON_SEPARATORS = "-\\.";
    +    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
    +    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
    +    private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
    +
    +    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+"))
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("day", optionally(DIGIT)+DIGIT);
    +    private final static String DATE_WORDS_2 = 
    +        namedGroup("month", anyChar(LETTER)+"+")
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
    +    private final static String DATE_WORDS_3 = 
    +        namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("month", anyChar(LETTER)+"+")
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +
    +    private final static String DATE_ONLY_NO_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + namedGroup("month", DIGIT+DIGIT)
    +            + namedGroup("day", DIGIT+DIGIT);
    +
    +    private final static String MERIDIAN = anyChar("aApP")+optionally(anyChar("mM"));
    +    private final static String TIME_ONLY_WITH_INNER_SEPARATORS = 
    +        namedGroup("hours", optionally(DIGIT)+DIGIT)+
    +        optionally(
    +            anyChar(TIME_SEPARATOR)+
    +            namedGroup("mins", DIGIT+DIGIT)+
    +            optionally(
    +                anyChar(TIME_SEPARATOR)+
    +                namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+"))))+
    +        optionally(" *" + namedGroup("meridian", notMatching(LETTER+LETTER+LETTER)+MERIDIAN));
    +    private final static String TIME_ONLY_NO_SEPARATORS = 
    +        namedGroup("hours", DIGIT+DIGIT)+
    +        namedGroup("mins", DIGIT+DIGIT)+
    +        optionally(
    +            namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+")))+
    +        namedGroup("meridian", "");
    +
    +    private final static String TZ_CODE = namedGroup("tzCode",
    +        notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) // not AM or PM
    +        + anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*");
    +    private final static String TIME_ZONE_SIGNED_OFFSET = namedGroup("tz", options(namedGroup("tzOffset", options("\\+", "-")+
    +            DIGIT+optionally(DIGIT)+optionally(optionally(":")+DIGIT+DIGIT)), 
    +        optionally("\\+")+TZ_CODE));
    +    private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET = namedGroup("tz", 
    +        options(
    +            namedGroup("tzOffset", options("\\+", "-", " ")+
    +                options("0"+DIGIT, "10", "11", "12")+optionally(optionally(":")+DIGIT+DIGIT)), 
    +            TZ_CODE));
    +
    +    private static String getDateTimeSeparatorPattern(String extraChars) {
    +        return options(" +"+optionally(anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")),
    +                anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+","))
    +            + anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars)+"*";
    +    }
    +    
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarSimpleFlexibleFormatParser(String input) {
    +        input = input.trim();
    +
    +        String[] DATE_PATTERNS = new String[] {
    +            DATE_ONLY_WITH_INNER_SEPARATORS,
    +            DATE_ONLY_NO_SEPARATORS,
    +            DATE_WORDS_2,
    +            DATE_WORDS_3,            
    +        };
    +        String[] TIME_PATTERNS = new String[] {
    +            TIME_ONLY_WITH_INNER_SEPARATORS,
    +            TIME_ONLY_NO_SEPARATORS            
    +        };
    +        String[] TZ_PATTERNS = new String[] {
    +            // space then time zone with sign (+-) or code is preferred
    +            optionally(getDateTimeSeparatorPattern("")) + " " + TIME_ZONE_SIGNED_OFFSET,
    +            // then no TZ - but declare the named groups
    +            namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", "")),
    +            // then any separator then offset with sign
    +            getDateTimeSeparatorPattern("") + TIME_ZONE_SIGNED_OFFSET,
    +            
    +            // try parsing with enforced separators before TZ first 
    +            // (so e.g. in the case of DATE-0100, the -0100 is the time, not the timezone)
    +            // then relax below (e.g. in the case of DATE-TIME+0100)
    +            
    +            // finally match DATE-TIME-1000 as time zone -1000
    +            // or DATE-TIME 1000 as TZ +1000 in case a + was supplied but converted to ' ' by web
    +            // (but be stricter about the format, two or four digits required, and hours <= 12 so as not to confuse with a year)
    +            optionally(getDateTimeSeparatorPattern("")) + TIME_ZONE_OPTIONALLY_SIGNED_OFFSET
    +        };
    +        
    +        List<String> basePatterns = MutableList.of();
    +        
    +        // patterns with date first
    +        String[] DATE_PATTERNS_UNCLOSED = new String[] {
    +            // separator before time *required* if date had separators
    +            DATE_ONLY_WITH_INNER_SEPARATORS + "("+getDateTimeSeparatorPattern("Tt"),
    +            // separator before time optional if date did not have separators
    +            DATE_ONLY_NO_SEPARATORS + "("+optionally(getDateTimeSeparatorPattern("Tt")),
    +            // separator before time required if date has words
    +            DATE_WORDS_2 + "("+getDateTimeSeparatorPattern("Tt"),
    +            DATE_WORDS_3 + "("+getDateTimeSeparatorPattern("Tt"),
    +        };
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS_UNCLOSED)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + dateP + timeP+")?" + tzP + "$");
    +        
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + getDateTimeSeparatorPattern("") + dateP + tzP + "$");
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + tzP + getDateTimeSeparatorPattern("") + dateP + "$");
    +
    +        Maybe<Matcher> mm = Maybe.absent();
    +        for (String p: basePatterns) {
    +            mm = match(p, input);
    +            if (mm.isPresent()) break;
    +        }
    +        if (mm.isPresent()) {
    +            Matcher m = mm.get();
    +            Calendar result;
    +
    +            String tz = m.group("tz");
    +            
    +            int year = Integer.parseInt(m.group("year"));
    +            int day = Integer.parseInt(m.group("day"));
    +            
    +            String monthS = m.group("month");
    +            int month;
    +            if (monthS.matches(DIGIT+"+")) {
    +                month = Integer.parseInt(monthS)-1;
    +            } else {
    +                try {
    +                    month = new SimpleDateFormat("yyyy-MMM-dd").parse("2015-"+monthS+"-15").getMonth();
    +                } catch (ParseException e) {
    +                    return Maybe.absent("Unknown date format '"+input+"': invalid month '"+monthS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                }
    +            }
    +            
    +            if (Strings.isNonBlank(tz)) {
    +                TimeZone tzz = null;
    +                String tzCode = m.group("tzCode");
    +                if (Strings.isNonBlank(tzCode)) {
    +                    tz = tzCode;
    +                }
    +                if (tz.matches(DIGIT+"+")) {
    +                    // stick a plus in front in case it was submitted by a web form and turned into a space
    +                    tz = "+"+tz;
    +                } else {
    +                    tzz = getTimeZone(tz);
    +                }
    +                if (tzz==null) {
    +                    Maybe<Matcher> tmm = match("^ ?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")"+optionally(optionally(":")+namedGroup("tzM", DIGIT+DIGIT))+"$", tz);
    +                    if (tmm.isAbsent()) {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid timezone '"+tz+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    Matcher tm = tmm.get();
    +                    String tzM = tm.group("tzM");
    +                    int offset = (60*Integer.parseInt(tm.group("tzH")) + Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60;
    +                    tzz = new SimpleTimeZone(offset*1000, tz);
    +                }
    +                tz = getTimeZoneOffsetString(tzz, year, month, day);
    +                result = new GregorianCalendar(tzz);
    +            } else {
    +                result = new GregorianCalendar();
    +            }
    +            result.clear();
    +            
    +            result.set(Calendar.YEAR, year);
    +            result.set(Calendar.MONTH, month);
    +            result.set(Calendar.DAY_OF_MONTH, day);
    +            if (m.group("hours")!=null) {
    +                int hours = Integer.parseInt(m.group("hours"));
    +                String meridian = m.group("meridian");
    +                if (Strings.isNonBlank(meridian) && meridian.toLowerCase().startsWith("p")) {
    +                    if (hours>12) {
    +                        return Maybe.absent("Unknown date format '"+input+"': can't be "+hours+" PM; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    hours += 12;
    +                }
    +                result.set(Calendar.HOUR_OF_DAY, hours);
    +                String minsS = m.group("mins");
    +                if (Strings.isNonBlank(minsS)) {
    +                    result.set(Calendar.MINUTE, Integer.parseInt(minsS));
    +                }
    +                String secsS = m.group("secs");
    +                if (Strings.isBlank(secsS)) {
    +                    // leave at zero
    +                } else if (secsS.matches(DIGIT+DIGIT+"?")) {
    +                    result.set(Calendar.SECOND, Integer.parseInt(secsS));
    +                } else {
    +                    double s = Double.parseDouble(secsS);
    +                    if (s>=0 && s<=60) {
    +                        // in double format, with correct period
    +                    } else if (secsS.length()==5) {
    +                        // allow ssSSS with no punctuation
    +                        s = s/=1000;
    +                    } else {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid seconds '"+secsS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    result.set(Calendar.SECOND, (int)s);
    +                    result.set(Calendar.MILLISECOND, (int)((s*1000) % 1000));
    +                }
    +            }
    +            
    +            return Maybe.of(result);
    +        }
    +        return Maybe.absent("Unknown date format '"+input+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +    }
    +    
    +    public static TimeZone getTimeZone(String code) {
    +        if (code.indexOf('/')==-1) {
    +            if ("Z".equals(code)) return TIME_ZONE_UTC;
    +            if ("UTC".equals(code)) return TIME_ZONE_UTC;
    +            if ("GMT".equals(code)) return TIME_ZONE_UTC;
    +            
    +            // get the time zone -- most short codes aren't accepted, so accept (and prefer) certain common codes
    +            if ("EST".equals(code)) return getTimeZone("America/New_York");
    +            if ("EDT".equals(code)) return getTimeZone("America/New_York");
    +            if ("PST".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("CST".equals(code)) return getTimeZone("America/Chicago");
    +            if ("CDT".equals(code)) return getTimeZone("America/Chicago");
    +            if ("MST".equals(code)) return getTimeZone("America/Denver");
    +            if ("MDT".equals(code)) return getTimeZone("America/Denver");
    +
    +            if ("BST".equals(code)) return getTimeZone("Europe/London");  // otherwise BST is Bangladesh!
    +            if ("CEST".equals(code)) return getTimeZone("Europe/Paris");
    +            // IST falls through to below, where it is treated as India (not Irish); IDT not recognised
    +        }
    +        
    +        TimeZone tz = TimeZone.getTimeZone(code);
    +        if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) {
    +            // recognized
    +            return tz;
    +        }
    +        // possibly unrecognized -- GMT returned if not known, bad TimeZone API!
    +        String timeZones[] = TimeZone.getAvailableIDs();
    +        for (String tzs: timeZones) {
    +            if (tzs.equals(code)) return tz;
    +        }
    +        // definitely unrecognized
    +        return null;
    +    }
    +    
    +    /** convert a TimeZone e.g. Europe/London to an offset string as at the given day, e.g. +0100 or +0000 depending daylight savings,
    +     * absent with nice error if zone unknown */
    +    public static Maybe<String> getTimeZoneOffsetString(String tz, int year, int month, int day) {
    +        TimeZone tzz = getTimeZone(tz);
    +        if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz);
    +        return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day));
    +    }
    +    
    +    /** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the {@link TimeZone} is already instantiated */
    +    @SuppressWarnings("deprecation")
    +    public static String getTimeZoneOffsetString(TimeZone tz, int year, int month, int day) {
    +        int tzMins = tz.getOffset(new Date(year, month, day).getTime())/60/1000;
    +        String tzStr = (tzMins<0 ? "-" : "+") + Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", "")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", "");
    +        return tzStr;
    +    }
    +
    +    private static String namedGroup(String name, String pattern) {
    +        return "(?<"+name+">"+pattern+")";
    +    }
    +    private static String anyChar(String charSet) {
    +        return "["+charSet+"]";
    +    }
    +    private static String optionally(String pattern) {
    +        return "("+pattern+")?";
    +    }
    +    private static String options(String ...patterns) {
    +        return "("+Strings.join(patterns,"|")+")";
    +    }
    +    private static String notMatching(String pattern) {
    +        return "(?!"+pattern+")";
    +    }
    +    
    +    private static Maybe<Matcher> match(String pattern, String input) {
    +        Matcher m = Pattern.compile(pattern).matcher(input);
    +        if (m.find() && m.start()==0 && m.end()==input.length())
    --- End diff --
    
    Could you prepend `^` and append `$` to pattern to achieve the same whole-string match? 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-109966335
  
    Joda Time's parser is slightly better than Java's but it too is extremely rigid.  We'll need different formatter specifiers for `.` vs `-` vs ` ` and `MM` vs `MMM` vs `MMMM` which becomes combinatorially tedious to express and slow to execute.  (Apache DateUtils is basically a wrapper around Java's so has even worse drawbacks.)  A flexible off-the-rack parser was my first hunt.  I tried `natty` for this reason (which is extremely cool but has drawbacks as noted in the PR).
    
    The other option is to mandate a single format but that makes things harder to work with as people then have to look up the syntax.  And although ISO 8601 is now a de facto standard for machines it makes for ugly blueprints.
    
    ---
    
    Generally in Brooklyn we try to be lenient about parsing and encourage natural-looking blueprints.  Users seem to react well to that.  I think the same is warranted for date support, and I think this custom parser is the best way.  (I'd prefer it more if this could become a standard part of `DateUtils` or similar but people tend to be opinionated on their dates!)



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on a diff in the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#discussion_r32332569
  
    --- Diff: utils/common/src/main/java/brooklyn/util/time/Time.java ---
    @@ -443,36 +500,466 @@ else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreC
             }
         }
     
    +    public static Calendar newCalendarFromMillisSinceEpochUtc(long timestamp) {
    +        GregorianCalendar cal = new GregorianCalendar();
    +        cal.setTimeInMillis(timestamp);
    +        return cal;
    +    }
    +
    +    public static Calendar newCalendarFromDate(Date date) {
    +        return newCalendarFromMillisSinceEpochUtc(date.getTime());
    +    }
    +    
    +    /** As {@link #parseCalendar(String)} but returning a {@link Date},
    +     * (i.e. a record where the time zone has been applied and forgotten). */
    +    public static Date parseDate(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get().getTime();
    +    }
    +
    +    /** Parses dates from string, accepting many formats including ISO-8601 and http://yaml.org/type/timestamp.html, 
    +     * e.g. 2015-06-15 16:00:00 +0000.
    +     * <p>
    +     * Millis since epoch (1970) is also supported to represent the epoch (0) or dates in this millenium,
    +     * but to prevent ambiguity of e.g. "20150615", any other dates prior to the year 2001 are not accepted.
    +     * (However if a type Long is supplied, e.g. from a YAML parse, it will always be treated as millis since epoch.) 
    +     * <p>
    +     * Other formats including locale-specific variants, e.g. recognising month names,
    +     * are supported but this may vary from platform to platform and may change between versions. */
    +    public static Calendar parseCalendar(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get();
    +    }
    +    
    +    /** as {@link #parseCalendar(String)} but returning a {@link Maybe} rather than throwing or returning null */
    +    public static Maybe<Calendar> parseCalendarMaybe(String input) {
    +        if (input==null) return Maybe.absent("value is null");
    +        input = input.trim();
    +        Maybe<Calendar> result;
    +
    +        result = parseCalendarUtc(input);
    +        if (result.isPresent()) return result;
    +
    +        result = parseCalendarSimpleFlexibleFormatParser(input);
    +        if (result.isPresent()) return result;
    +        // return the error from this method
    +        Maybe<Calendar> returnResult = result;
    +
    +//        // see natty method comments below
    +//        Maybe<Date> result = parseDateNatty(input);
    +//        if (result.isPresent()) return result;
    +
    +        result = parseCalendarFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
    +        if (result.isPresent()) return result;
    +        result = parseCalendarDefaultParse(input);
    +        if (result.isPresent()) return result;
    +
    +        return returnResult;
    +    }
    +
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarDefaultParse(String input) {
    +        try {
    +            long ms = Date.parse(input);
    +            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
    +                // accept default date parse for this century and next
    +                GregorianCalendar c = new GregorianCalendar();
    +                c.setTimeInMillis(ms);
    +                return Maybe.of((Calendar)c);
    +            }
    +        } catch (Exception e) {
    +            Exceptions.propagateIfFatal(e);
    +        }
    +        return Maybe.absent();
    +    }
    +
    +    private static Maybe<Calendar> parseCalendarUtc(String input) {
    +        input = input.trim();
    +        if (input.matches("\\d+")) {
    +            if ("0".equals(input)) {
    +                // accept 0 as epoch UTC
    +                return Maybe.of(newCalendarFromMillisSinceEpochUtc(0));
    +            }
    +            Maybe<Calendar> result = Maybe.of(newCalendarFromMillisSinceEpochUtc(Long.parseLong(input)));
    +            if (result.isPresent()) {
    +                int year = result.get().get(Calendar.YEAR);
    +                if (year >= 2000 && year < 2200) {
    +                    // only applicable for dates in this century
    +                    return result;
    +                } else {
    +                    return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
    +                }
    +            }
    +        }
    +        return Maybe.absent("not long millis since epoch UTC");
    +    }
    +
    +    private final static String DIGIT = "\\d";
    +    private final static String LETTER = "\\p{L}";
    +    private final static String COMMON_SEPARATORS = "-\\.";
    +    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
    +    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
    +    private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
    +
    +    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+"))
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("day", optionally(DIGIT)+DIGIT);
    +    private final static String DATE_WORDS_2 = 
    +        namedGroup("month", anyChar(LETTER)+"+")
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
    +    private final static String DATE_WORDS_3 = 
    +        namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("month", anyChar(LETTER)+"+")
    --- End diff --
    
    agree


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by alasdairhodge <gi...@git.apache.org>.
Github user alasdairhodge commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-109934408
  
    Actually, I'm inclined to agree with @sjcorbett. Better to mandate a standard format (ISO8601 is the obvious choice) and pull in a mature implementation such as joda-time or Apache commons DateUtils.
    
    Core Java has been moving in the right direction: Java 7's SimpleDateFormat supports ISO8601, and Java 8 includes the new java.time API (heavily influenced by joda-time).
    
    Lots of discussions on StackOverflow:
    http://stackoverflow.com/questions/2201925/converting-iso-8601-compliant-string-to-java-util-date


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-109988207
  
    @alasdairhodge that's the tosca mindset, it's great for systems but not for users.  a single format which many people don't know about (really a `T` in the middle??) is going to make it hard for people.
    
    nearly all the feedback from users has been to the contrary:  blueprints are best when intuitive and forgiving, so long as there isn't a risk associated or leaky complexity.
    
    it *is* good to recommend one way to do things, and we have been guilty of showing too many.  so i'm happy to recommend ISO 8601 but i think almost any human would rather see a space in the middle instead of the ugly `T`.  (of course the weirder your format the riskier it is, such as the weird examples in the test case.)
    
    we definitely can't rely on locale delimiters as the ultimate arbiter as sharing across servers is important.
    
    btw ISO 8601 supports bizarre things like `2015-156` for the 156th day of the year.  and it mandates a `P` prefix for durations `P3H`.  these would not be popular.
    
    OTOH being able to express a stabilization delay as `2m 30s` is extremely popular.  similarly `4g` for RAM instead of `4096` (where you used to have to look up the fact that it is `mB`).
    
    let's not make users' lives difficult!


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by alasdairhodge <gi...@git.apache.org>.
Github user alasdairhodge commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-110009980
  
    Oooh, see also: http://yaml.org/type/timestamp.html  Supports "relaxed" ISO8601 with spaces.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-111282625
  
    @sjcorbett @alasdairhodge do you feel strongly this should not be merged or would that be okay?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by sjcorbett <gi...@git.apache.org>.
Github user sjcorbett commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-110296770
  
    @ahgittin I think your comment that the "other option is to mandate a single format but that makes things harder to work with as people then have to look up the syntax" is erroneous - people will need to look up the syntax accepted by Brooklyn anyway. Why is "10.11-2020-12.01" read as "2020-12-01-1011" and not "2020-11-10 12:01"? Brooklyn shouldn't make the decision, the user should supply the format string for their bonkers date.
    
    We gain some flexibility and we gain a lot of ambiguity. To understand what Brooklyn will do with my date I need to understand about 400 lines of Java. I would prefer to just have to scan a bunch of DateFormatters, even if they are more expensive to express.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by alasdairhodge <gi...@git.apache.org>.
Github user alasdairhodge commented on a diff in the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#discussion_r31898443
  
    --- Diff: utils/common/src/main/java/brooklyn/util/time/Time.java ---
    @@ -443,36 +472,433 @@ else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreC
             }
         }
     
    +    /** parses dates from string, accepting many formats including 'YYYY-MM-DD', 'YYYY-MM-DD HH:mm:SS', or millis since UTC epoch */
    +    public static Date parseDate(String input) {
    +        if (input==null) return null;
    +        return parseDateMaybe(input).get();
    +    }
    +    
    +    /** as {@link #parseDate(String)} but returning a {@link Maybe} rather than throwing or returning null */
    +    public static Maybe<Date> parseDateMaybe(String input) {
    +        if (input==null) return Maybe.absent("value is null");
    +        input = input.trim();
    +        Maybe<Date> result;
    +
    +        result = parseDateUtc(input);
    +        if (result.isPresent()) return result;
    +
    +        result = parseDateSimpleFlexibleFormatParser(input);
    +        if (result.isPresent()) return result;
    +        // return the error from this method
    +        Maybe<Date> returnResult = result;
    +
    +//        // see natty method comments below
    +//        Maybe<Date> result = parseDateNatty(input);
    +//        if (result.isPresent()) return result;
    +
    +        result = parseDateFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
    +        if (result.isPresent()) return result;
    +        result = parseDateDefaultParse(input);
    +        if (result.isPresent()) return result;
    +
    +        return returnResult;
    +    }
    +
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Date> parseDateDefaultParse(String input) {
    +        try {
    +            long ms = Date.parse(input);
    +            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
    +                // accept default date parse for this century and next
    +                return Maybe.of(new Date(ms));
    +            }
    +        } catch (Exception e) {
    +            Exceptions.propagateIfFatal(e);
    +        }
    +        return Maybe.absent();
    +    }
    +
    +    private static Maybe<Date> parseDateUtc(String input) {
    +        if (input.matches("\\d+")) {
    +            Maybe<Date> result = Maybe.of(new Date(Long.parseLong(input)));
    +            if (result.isPresent()) {
    +                @SuppressWarnings("deprecation")
    +                int year = result.get().getYear();
    +                if (year >= 2000 && year < 2200) {
    +                    // only applicable for dates in this century
    +                    return result;
    +                } else {
    +                    return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
    +                }
    +            }
    +        }
    +        return Maybe.absent("not long millis since epoch UTC");
    +    }
    +
    +    private final static String DIGIT = "\\d";
    +    private final static String LETTER = "\\p{L}";
    +    private final static String COMMON_SEPARATORS = "-\\.";
    +    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
    +    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
    +    private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
    +
    +    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+"))
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("day", optionally(DIGIT)+DIGIT);
    +    private final static String DATE_WORDS_2 = 
    +        namedGroup("month", anyChar(LETTER)+"+")
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
    +    private final static String DATE_WORDS_3 = 
    +        namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("month", anyChar(LETTER)+"+")
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +
    +    private final static String DATE_ONLY_NO_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + namedGroup("month", DIGIT+DIGIT)
    +            + namedGroup("day", DIGIT+DIGIT);
    +
    +    private final static String MERIDIAN = anyChar("aApP")+optionally(anyChar("mM"));
    +    private final static String TIME_ONLY_WITH_INNER_SEPARATORS = 
    +        namedGroup("hours", optionally(DIGIT)+DIGIT)+
    +        optionally(
    +            anyChar(TIME_SEPARATOR)+
    +            namedGroup("mins", DIGIT+DIGIT)+
    +            optionally(
    +                anyChar(TIME_SEPARATOR)+
    +                namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+"))))+
    +        optionally(" *" + namedGroup("meridian", notMatching(LETTER+LETTER+LETTER)+MERIDIAN));
    +    private final static String TIME_ONLY_NO_SEPARATORS = 
    +        namedGroup("hours", DIGIT+DIGIT)+
    +        namedGroup("mins", DIGIT+DIGIT)+
    +        optionally(
    +            namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+")))+
    +        namedGroup("meridian", "");
    +
    +    private final static String TZ_CODE = namedGroup("tzCode",
    +        notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) // not AM or PM
    +        + anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*");
    +    private final static String TIME_ZONE_SIGNED_OFFSET = namedGroup("tz", options(namedGroup("tzOffset", options("\\+", "-")+
    +            DIGIT+optionally(DIGIT)+optionally(optionally(":")+DIGIT+DIGIT)), 
    +        optionally("\\+")+TZ_CODE));
    +    private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET = namedGroup("tz", 
    +        options(
    +            namedGroup("tzOffset", options("\\+", "-", " ")+
    +                options("0"+DIGIT, "10", "11", "12")+optionally(optionally(":")+DIGIT+DIGIT)), 
    +            TZ_CODE));
    +
    +    private static String getDateTimeSeparatorPattern(String extraChars) {
    +        return options(" +"+optionally(anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")),
    +                anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+","))
    +            + anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars)+"*";
    +    }
    +    
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Date> parseDateSimpleFlexibleFormatParser(String input) {
    +        input = input.trim();
    +
    +        String[] DATE_PATTERNS = new String[] {
    +            DATE_ONLY_WITH_INNER_SEPARATORS,
    +            DATE_ONLY_NO_SEPARATORS,
    +            DATE_WORDS_2,
    +            DATE_WORDS_3,            
    +        };
    +        String[] TIME_PATTERNS = new String[] {
    +            TIME_ONLY_WITH_INNER_SEPARATORS,
    +            TIME_ONLY_NO_SEPARATORS            
    +        };
    +        String[] TZ_PATTERNS = new String[] {
    +            // space then time zone with sign (+-) or code is preferred
    +            optionally(getDateTimeSeparatorPattern("")) + " " + TIME_ZONE_SIGNED_OFFSET,
    +            // then no TZ - but declare the named groups
    +            namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", "")),
    +            // then any separator then offset with sign
    +            getDateTimeSeparatorPattern("") + TIME_ZONE_SIGNED_OFFSET,
    +            
    +            // try parsing with enforced separators before TZ first 
    +            // (so e.g. in the case of DATE-0100, the -0100 is the time, not the timezone)
    +            // then relax below (e.g. in the case of DATE-TIME+0100)
    +            
    +            // finally match DATE-TIME-1000 as time zone -1000
    +            // or DATE-TIME 1000 as TZ +1000 in case a + was supplied but converted to ' ' by web
    +            // (but be stricter about the format, two or four digits required, and hours <= 12 so as not to confuse with a year)
    +            optionally(getDateTimeSeparatorPattern("")) + TIME_ZONE_OPTIONALLY_SIGNED_OFFSET
    +        };
    +        
    +        List<String> basePatterns = MutableList.of();
    +        
    +        // patterns with date first
    +        String[] DATE_PATTERNS_UNCLOSED = new String[] {
    +            // separator before time *required* if date had separators
    +            DATE_ONLY_WITH_INNER_SEPARATORS + "("+getDateTimeSeparatorPattern("T"),
    +            // separator before time optional if date did not have separators
    +            DATE_ONLY_NO_SEPARATORS + "("+optionally(getDateTimeSeparatorPattern("T")),
    +            // separator before time required if date has words
    +            DATE_WORDS_2 + "("+getDateTimeSeparatorPattern("T"),
    +            DATE_WORDS_3 + "("+getDateTimeSeparatorPattern("T"),
    +        };
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS_UNCLOSED)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + dateP + timeP+")?" + tzP + "$");
    +        
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + getDateTimeSeparatorPattern("") + dateP + tzP + "$");
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + tzP + getDateTimeSeparatorPattern("") + dateP + "$");
    +
    +        Maybe<Matcher> mm = Maybe.absent();
    +        for (String p: basePatterns) {
    +            mm = match(p, input);
    +            if (mm.isPresent()) break;
    +        }
    +        if (mm.isPresent()) {
    +            Matcher m = mm.get();
    +            Calendar result;
    +
    +            String tz = m.group("tz");
    +            
    +            int year = Integer.parseInt(m.group("year"));
    +            int day = Integer.parseInt(m.group("day"));
    +            
    +            String monthS = m.group("month");
    +            int month;
    +            if (monthS.matches(DIGIT+"+")) {
    +                month = Integer.parseInt(monthS)-1;
    +            } else {
    +                try {
    +                    month = new SimpleDateFormat("yyyy-MMM-dd").parse("2015-"+monthS+"-15").getMonth();
    +                } catch (ParseException e) {
    +                    return Maybe.absent("Unknown date format '"+input+"': invalid month '"+monthS+"'; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
    +                }
    +            }
    +            
    +            if (Strings.isNonBlank(tz)) {
    +                TimeZone tzz = null;
    +                String tzCode = m.group("tzCode");
    +                if (Strings.isNonBlank(tzCode)) {
    +                    tz = tzCode;
    +                }
    +                if (tz.matches(DIGIT+"+")) {
    +                    // stick a plus in front in case it was submitted by a web form and turned into a space
    +                    tz = "+"+tz;
    +                } else {
    +                    tzz = getTimeZone(tz);
    +                }
    +                if (tzz==null) {
    +                    Maybe<Matcher> tmm = match("^ ?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")"+optionally(optionally(":")+namedGroup("tzM", DIGIT+DIGIT))+"$", tz);
    +                    if (tmm.isAbsent()) {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid timezone '"+tz+"'; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
    +                    }
    +                    Matcher tm = tmm.get();
    +                    String tzM = tm.group("tzM");
    +                    int offset = (60*Integer.parseInt(tm.group("tzH")) + Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60;
    +                    tzz = new SimpleTimeZone(offset*1000, tz);
    +                }
    +                tz = getTimeZoneOffsetString(tzz, year, month, day);
    +                result = new GregorianCalendar(tzz);
    +            } else {
    +                result = new GregorianCalendar();
    +            }
    +            result.clear();
    +            
    +            result.set(Calendar.YEAR, year);
    +            result.set(Calendar.MONTH, month);
    +            result.set(Calendar.DAY_OF_MONTH, day);
    +            if (m.group("hours")!=null) {
    +                int hours = Integer.parseInt(m.group("hours"));
    +                String meridian = m.group("meridian");
    +                if (Strings.isNonBlank(meridian) && meridian.toLowerCase().startsWith("p")) {
    +                    if (hours>12) {
    +                        return Maybe.absent("Unknown date format '"+input+"': can't be "+hours+" PM; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
    +                    }
    +                    hours += 12;
    +                }
    +                result.set(Calendar.HOUR_OF_DAY, hours);
    +                String minsS = m.group("mins");
    +                if (Strings.isNonBlank(minsS)) {
    +                    result.set(Calendar.MINUTE, Integer.parseInt(minsS));
    +                }
    +                String secsS = m.group("secs");
    +                if (Strings.isBlank(secsS)) {
    +                    // leave at zero
    +                } else if (secsS.matches(DIGIT+DIGIT+"?")) {
    +                    result.set(Calendar.SECOND, Integer.parseInt(secsS));
    +                } else {
    +                    double s = Double.parseDouble(secsS);
    +                    if (s>=0 && s<=60) {
    +                        // in double format, with correct period
    +                    } else if (secsS.length()==5) {
    +                        // allow ssSSS with no punctuation
    +                        s = s/=1000;
    +                    } else {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid seconds '"+secsS+"'; try 'YYYY-MM-DD HH:mm:ss.SSS +0000'");
    +                    }
    +                    result.set(Calendar.SECOND, (int)s);
    +                    result.set(Calendar.MILLISECOND, (int)((s*1000) % 1000));
    +                }
    +            }
    +            
    +            return Maybe.of(result.getTime());
    +        }
    +        return Maybe.absent("Unknown date format '"+input+"'; try ISO-8601, or 'yyyy-MM-dd' or 'yyyy-MM-dd HH:mm:ss +0000'");
    +    }
    +    
    +    public static TimeZone getTimeZone(String code) {
    +        if (code.indexOf('/')==-1) {
    +            if ("Z".equals(code)) return getTimeZone("UTC");
    +            
    +            // get the time zone -- most short codes aren't accepted, so accept (and prefer) certain common codes
    +            if ("EST".equals(code)) return getTimeZone("America/New_York");
    +            if ("EDT".equals(code)) return getTimeZone("America/New_York");
    +            if ("PST".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("CST".equals(code)) return getTimeZone("America/Chicago");
    +            if ("CDT".equals(code)) return getTimeZone("America/Chicago");
    +            if ("MST".equals(code)) return getTimeZone("America/Denver");
    +            if ("MDT".equals(code)) return getTimeZone("America/Denver");
    +
    +            if ("BST".equals(code)) return getTimeZone("Europe/London");  // otherwise BST is Bangladesh!
    +            if ("CEST".equals(code)) return getTimeZone("Europe/Paris");
    +            // IST falls through to below, where it is treated as India (not Irish); IDT not recognised
    +        }
    +        
    +        TimeZone tz = TimeZone.getTimeZone(code);
    +        if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) {
    +            // recognized
    +            return tz;
    +        }
    +        // possibly unrecognized -- GMT returned if not known, bad TimeZone API!
    +        String timeZones[] = TimeZone.getAvailableIDs();
    +        for (String tzs: timeZones) {
    +            if (tzs.equals(code)) return tz;
    +        }
    +        // definitely unrecognized
    +        return null;
    +    }
    +    
    +    /** convert a TimeZone e.g. Europe/London to an offset string as at the given day, e.g. +0100 or +0000 depending daylight savings,
    +     * absent with nice error if zone unknown */
    +    public static Maybe<String> getTimeZoneOffsetString(String tz, int year, int month, int day) {
    +        TimeZone tzz = getTimeZone(tz);
    +        if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz);
    +        return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day));
    +    }
    +    
    +    /** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the {@link TimeZone} is already instantiated */
    +    @SuppressWarnings("deprecation")
    +    public static String getTimeZoneOffsetString(TimeZone tz, int year, int month, int day) {
    +        int tzMins = tz.getOffset(new Date(year, month, day).getTime())/60/1000;
    +        String tzStr = (tzMins<0 ? "-" : "+") + Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", "")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", "");
    +        return tzStr;
    +    }
    +
    +    private static String namedGroup(String name, String pattern) {
    +        return "(?<"+name+">"+pattern+")";
    +    }
    +    private static String anyChar(String charSet) {
    +        return "["+charSet+"]";
    +    }
    +    private static String optionally(String pattern) {
    +        return "("+pattern+")?";
    --- End diff --
    
    Would a non-capturing group be better?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by sjcorbett <gi...@git.apache.org>.
Github user sjcorbett commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-111431341
  
    @ahgittin Thanks for asking. Made a few minor points. I'm alright seeing this merged.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
GitHub user ahgittin reopened a pull request:

    https://github.com/apache/incubator-brooklyn/pull/682

    Date parsing and type coercion fixes, require Java 7+

    

You can merge this pull request into a Git repository by running:

    $ git pull https://github.com/ahgittin/incubator-brooklyn misc-fixes

Alternatively you can review and apply these changes as the patch at:

    https://github.com/apache/incubator-brooklyn/pull/682.patch

To close this pull request, make a commit to your master/trunk branch
with (at least) the following in the commit message:

    This closes #682
    
----
commit 9fe4c7c413e6aeba6ec01cf2e10427d2829b8cb2
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Date:   2015-06-04T15:53:17Z

    clean up time API and support date parsing
    
    also a lot of help for time zones, because java seems bad with this,
    and making time methods consistent and less ambiguous

commit 30c3029ef22bb387ef121cc043bcfd495cfc9837
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Date:   2015-06-04T15:53:41Z

    better REST coercion and coerce String->Date
    
    wrt REST this improves how deferred suppliers are evaluated,
    both timing and in entity's execution context

commit 0988b6d8f58bea06fe81191eddba691140fde5f1
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Date:   2015-06-08T08:36:46Z

    require java 7
    
    needed for named groups in regexes;
    also fix semantics of 'T' separator and tidy date parsing

commit c968211642d9e4c8264469aa34b64f6dc43226de
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Date:   2015-06-08T15:51:37Z

    date parsing - support `t` separator, and better doc, recommending yaml "timestamp"

commit 51ba0aaf173259881476b88ce37456a5e0665d0a
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Date:   2015-06-09T10:17:22Z

    switch Time api around dates to be based on Calendar so we preserve time zone

----


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by sjcorbett <gi...@git.apache.org>.
Github user sjcorbett commented on a diff in the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#discussion_r32301664
  
    --- Diff: utils/common/src/main/java/brooklyn/util/time/Time.java ---
    @@ -443,36 +500,466 @@ else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreC
             }
         }
     
    +    public static Calendar newCalendarFromMillisSinceEpochUtc(long timestamp) {
    +        GregorianCalendar cal = new GregorianCalendar();
    +        cal.setTimeInMillis(timestamp);
    +        return cal;
    +    }
    +
    +    public static Calendar newCalendarFromDate(Date date) {
    +        return newCalendarFromMillisSinceEpochUtc(date.getTime());
    +    }
    +    
    +    /** As {@link #parseCalendar(String)} but returning a {@link Date},
    +     * (i.e. a record where the time zone has been applied and forgotten). */
    +    public static Date parseDate(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get().getTime();
    +    }
    +
    +    /** Parses dates from string, accepting many formats including ISO-8601 and http://yaml.org/type/timestamp.html, 
    +     * e.g. 2015-06-15 16:00:00 +0000.
    +     * <p>
    +     * Millis since epoch (1970) is also supported to represent the epoch (0) or dates in this millenium,
    +     * but to prevent ambiguity of e.g. "20150615", any other dates prior to the year 2001 are not accepted.
    +     * (However if a type Long is supplied, e.g. from a YAML parse, it will always be treated as millis since epoch.) 
    +     * <p>
    +     * Other formats including locale-specific variants, e.g. recognising month names,
    +     * are supported but this may vary from platform to platform and may change between versions. */
    +    public static Calendar parseCalendar(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get();
    +    }
    +    
    +    /** as {@link #parseCalendar(String)} but returning a {@link Maybe} rather than throwing or returning null */
    +    public static Maybe<Calendar> parseCalendarMaybe(String input) {
    +        if (input==null) return Maybe.absent("value is null");
    +        input = input.trim();
    +        Maybe<Calendar> result;
    +
    +        result = parseCalendarUtc(input);
    +        if (result.isPresent()) return result;
    +
    +        result = parseCalendarSimpleFlexibleFormatParser(input);
    +        if (result.isPresent()) return result;
    +        // return the error from this method
    +        Maybe<Calendar> returnResult = result;
    +
    +//        // see natty method comments below
    +//        Maybe<Date> result = parseDateNatty(input);
    +//        if (result.isPresent()) return result;
    +
    +        result = parseCalendarFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
    +        if (result.isPresent()) return result;
    +        result = parseCalendarDefaultParse(input);
    +        if (result.isPresent()) return result;
    +
    +        return returnResult;
    +    }
    +
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarDefaultParse(String input) {
    +        try {
    +            long ms = Date.parse(input);
    +            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
    +                // accept default date parse for this century and next
    +                GregorianCalendar c = new GregorianCalendar();
    +                c.setTimeInMillis(ms);
    +                return Maybe.of((Calendar)c);
    +            }
    +        } catch (Exception e) {
    +            Exceptions.propagateIfFatal(e);
    +        }
    +        return Maybe.absent();
    +    }
    +
    +    private static Maybe<Calendar> parseCalendarUtc(String input) {
    +        input = input.trim();
    +        if (input.matches("\\d+")) {
    +            if ("0".equals(input)) {
    +                // accept 0 as epoch UTC
    +                return Maybe.of(newCalendarFromMillisSinceEpochUtc(0));
    +            }
    +            Maybe<Calendar> result = Maybe.of(newCalendarFromMillisSinceEpochUtc(Long.parseLong(input)));
    +            if (result.isPresent()) {
    +                int year = result.get().get(Calendar.YEAR);
    +                if (year >= 2000 && year < 2200) {
    +                    // only applicable for dates in this century
    +                    return result;
    +                } else {
    +                    return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
    +                }
    +            }
    +        }
    +        return Maybe.absent("not long millis since epoch UTC");
    +    }
    +
    +    private final static String DIGIT = "\\d";
    +    private final static String LETTER = "\\p{L}";
    +    private final static String COMMON_SEPARATORS = "-\\.";
    +    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
    +    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
    +    private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
    +
    +    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+"))
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("day", optionally(DIGIT)+DIGIT);
    +    private final static String DATE_WORDS_2 = 
    +        namedGroup("month", anyChar(LETTER)+"+")
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
    +    private final static String DATE_WORDS_3 = 
    +        namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("month", anyChar(LETTER)+"+")
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +
    +    private final static String DATE_ONLY_NO_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + namedGroup("month", DIGIT+DIGIT)
    +            + namedGroup("day", DIGIT+DIGIT);
    +
    +    private final static String MERIDIAN = anyChar("aApP")+optionally(anyChar("mM"));
    +    private final static String TIME_ONLY_WITH_INNER_SEPARATORS = 
    +        namedGroup("hours", optionally(DIGIT)+DIGIT)+
    +        optionally(
    +            anyChar(TIME_SEPARATOR)+
    +            namedGroup("mins", DIGIT+DIGIT)+
    +            optionally(
    +                anyChar(TIME_SEPARATOR)+
    +                namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+"))))+
    +        optionally(" *" + namedGroup("meridian", notMatching(LETTER+LETTER+LETTER)+MERIDIAN));
    +    private final static String TIME_ONLY_NO_SEPARATORS = 
    +        namedGroup("hours", DIGIT+DIGIT)+
    +        namedGroup("mins", DIGIT+DIGIT)+
    +        optionally(
    +            namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+")))+
    +        namedGroup("meridian", "");
    +
    +    private final static String TZ_CODE = namedGroup("tzCode",
    +        notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) // not AM or PM
    +        + anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*");
    +    private final static String TIME_ZONE_SIGNED_OFFSET = namedGroup("tz", options(namedGroup("tzOffset", options("\\+", "-")+
    +            DIGIT+optionally(DIGIT)+optionally(optionally(":")+DIGIT+DIGIT)), 
    +        optionally("\\+")+TZ_CODE));
    +    private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET = namedGroup("tz", 
    +        options(
    +            namedGroup("tzOffset", options("\\+", "-", " ")+
    +                options("0"+DIGIT, "10", "11", "12")+optionally(optionally(":")+DIGIT+DIGIT)), 
    +            TZ_CODE));
    +
    +    private static String getDateTimeSeparatorPattern(String extraChars) {
    +        return options(" +"+optionally(anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")),
    +                anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+","))
    +            + anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars)+"*";
    +    }
    +    
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarSimpleFlexibleFormatParser(String input) {
    +        input = input.trim();
    +
    +        String[] DATE_PATTERNS = new String[] {
    +            DATE_ONLY_WITH_INNER_SEPARATORS,
    +            DATE_ONLY_NO_SEPARATORS,
    +            DATE_WORDS_2,
    +            DATE_WORDS_3,            
    +        };
    +        String[] TIME_PATTERNS = new String[] {
    +            TIME_ONLY_WITH_INNER_SEPARATORS,
    +            TIME_ONLY_NO_SEPARATORS            
    +        };
    +        String[] TZ_PATTERNS = new String[] {
    +            // space then time zone with sign (+-) or code is preferred
    +            optionally(getDateTimeSeparatorPattern("")) + " " + TIME_ZONE_SIGNED_OFFSET,
    +            // then no TZ - but declare the named groups
    +            namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", "")),
    +            // then any separator then offset with sign
    +            getDateTimeSeparatorPattern("") + TIME_ZONE_SIGNED_OFFSET,
    +            
    +            // try parsing with enforced separators before TZ first 
    +            // (so e.g. in the case of DATE-0100, the -0100 is the time, not the timezone)
    +            // then relax below (e.g. in the case of DATE-TIME+0100)
    +            
    +            // finally match DATE-TIME-1000 as time zone -1000
    +            // or DATE-TIME 1000 as TZ +1000 in case a + was supplied but converted to ' ' by web
    +            // (but be stricter about the format, two or four digits required, and hours <= 12 so as not to confuse with a year)
    +            optionally(getDateTimeSeparatorPattern("")) + TIME_ZONE_OPTIONALLY_SIGNED_OFFSET
    +        };
    +        
    +        List<String> basePatterns = MutableList.of();
    +        
    +        // patterns with date first
    +        String[] DATE_PATTERNS_UNCLOSED = new String[] {
    +            // separator before time *required* if date had separators
    +            DATE_ONLY_WITH_INNER_SEPARATORS + "("+getDateTimeSeparatorPattern("Tt"),
    +            // separator before time optional if date did not have separators
    +            DATE_ONLY_NO_SEPARATORS + "("+optionally(getDateTimeSeparatorPattern("Tt")),
    +            // separator before time required if date has words
    +            DATE_WORDS_2 + "("+getDateTimeSeparatorPattern("Tt"),
    +            DATE_WORDS_3 + "("+getDateTimeSeparatorPattern("Tt"),
    +        };
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS_UNCLOSED)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + dateP + timeP+")?" + tzP + "$");
    +        
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + getDateTimeSeparatorPattern("") + dateP + tzP + "$");
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + tzP + getDateTimeSeparatorPattern("") + dateP + "$");
    +
    +        Maybe<Matcher> mm = Maybe.absent();
    +        for (String p: basePatterns) {
    +            mm = match(p, input);
    +            if (mm.isPresent()) break;
    +        }
    +        if (mm.isPresent()) {
    +            Matcher m = mm.get();
    +            Calendar result;
    +
    +            String tz = m.group("tz");
    +            
    +            int year = Integer.parseInt(m.group("year"));
    +            int day = Integer.parseInt(m.group("day"));
    +            
    +            String monthS = m.group("month");
    +            int month;
    +            if (monthS.matches(DIGIT+"+")) {
    +                month = Integer.parseInt(monthS)-1;
    +            } else {
    +                try {
    +                    month = new SimpleDateFormat("yyyy-MMM-dd").parse("2015-"+monthS+"-15").getMonth();
    +                } catch (ParseException e) {
    +                    return Maybe.absent("Unknown date format '"+input+"': invalid month '"+monthS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                }
    +            }
    +            
    +            if (Strings.isNonBlank(tz)) {
    +                TimeZone tzz = null;
    +                String tzCode = m.group("tzCode");
    +                if (Strings.isNonBlank(tzCode)) {
    +                    tz = tzCode;
    +                }
    +                if (tz.matches(DIGIT+"+")) {
    +                    // stick a plus in front in case it was submitted by a web form and turned into a space
    +                    tz = "+"+tz;
    +                } else {
    +                    tzz = getTimeZone(tz);
    +                }
    +                if (tzz==null) {
    +                    Maybe<Matcher> tmm = match("^ ?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")"+optionally(optionally(":")+namedGroup("tzM", DIGIT+DIGIT))+"$", tz);
    +                    if (tmm.isAbsent()) {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid timezone '"+tz+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    Matcher tm = tmm.get();
    +                    String tzM = tm.group("tzM");
    +                    int offset = (60*Integer.parseInt(tm.group("tzH")) + Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60;
    +                    tzz = new SimpleTimeZone(offset*1000, tz);
    +                }
    +                tz = getTimeZoneOffsetString(tzz, year, month, day);
    +                result = new GregorianCalendar(tzz);
    +            } else {
    +                result = new GregorianCalendar();
    +            }
    +            result.clear();
    +            
    +            result.set(Calendar.YEAR, year);
    +            result.set(Calendar.MONTH, month);
    +            result.set(Calendar.DAY_OF_MONTH, day);
    +            if (m.group("hours")!=null) {
    +                int hours = Integer.parseInt(m.group("hours"));
    +                String meridian = m.group("meridian");
    +                if (Strings.isNonBlank(meridian) && meridian.toLowerCase().startsWith("p")) {
    +                    if (hours>12) {
    +                        return Maybe.absent("Unknown date format '"+input+"': can't be "+hours+" PM; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    hours += 12;
    +                }
    +                result.set(Calendar.HOUR_OF_DAY, hours);
    +                String minsS = m.group("mins");
    +                if (Strings.isNonBlank(minsS)) {
    +                    result.set(Calendar.MINUTE, Integer.parseInt(minsS));
    +                }
    +                String secsS = m.group("secs");
    +                if (Strings.isBlank(secsS)) {
    +                    // leave at zero
    +                } else if (secsS.matches(DIGIT+DIGIT+"?")) {
    +                    result.set(Calendar.SECOND, Integer.parseInt(secsS));
    +                } else {
    +                    double s = Double.parseDouble(secsS);
    +                    if (s>=0 && s<=60) {
    +                        // in double format, with correct period
    +                    } else if (secsS.length()==5) {
    +                        // allow ssSSS with no punctuation
    +                        s = s/=1000;
    +                    } else {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid seconds '"+secsS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    result.set(Calendar.SECOND, (int)s);
    +                    result.set(Calendar.MILLISECOND, (int)((s*1000) % 1000));
    +                }
    +            }
    +            
    +            return Maybe.of(result);
    +        }
    +        return Maybe.absent("Unknown date format '"+input+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +    }
    +    
    +    public static TimeZone getTimeZone(String code) {
    +        if (code.indexOf('/')==-1) {
    +            if ("Z".equals(code)) return TIME_ZONE_UTC;
    +            if ("UTC".equals(code)) return TIME_ZONE_UTC;
    +            if ("GMT".equals(code)) return TIME_ZONE_UTC;
    +            
    +            // get the time zone -- most short codes aren't accepted, so accept (and prefer) certain common codes
    +            if ("EST".equals(code)) return getTimeZone("America/New_York");
    +            if ("EDT".equals(code)) return getTimeZone("America/New_York");
    +            if ("PST".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("CST".equals(code)) return getTimeZone("America/Chicago");
    +            if ("CDT".equals(code)) return getTimeZone("America/Chicago");
    +            if ("MST".equals(code)) return getTimeZone("America/Denver");
    +            if ("MDT".equals(code)) return getTimeZone("America/Denver");
    +
    +            if ("BST".equals(code)) return getTimeZone("Europe/London");  // otherwise BST is Bangladesh!
    +            if ("CEST".equals(code)) return getTimeZone("Europe/Paris");
    +            // IST falls through to below, where it is treated as India (not Irish); IDT not recognised
    +        }
    +        
    +        TimeZone tz = TimeZone.getTimeZone(code);
    +        if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) {
    +            // recognized
    +            return tz;
    +        }
    +        // possibly unrecognized -- GMT returned if not known, bad TimeZone API!
    +        String timeZones[] = TimeZone.getAvailableIDs();
    +        for (String tzs: timeZones) {
    +            if (tzs.equals(code)) return tz;
    +        }
    +        // definitely unrecognized
    +        return null;
    +    }
    +    
    +    /** convert a TimeZone e.g. Europe/London to an offset string as at the given day, e.g. +0100 or +0000 depending daylight savings,
    +     * absent with nice error if zone unknown */
    +    public static Maybe<String> getTimeZoneOffsetString(String tz, int year, int month, int day) {
    +        TimeZone tzz = getTimeZone(tz);
    +        if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz);
    +        return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day));
    +    }
    +    
    +    /** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the {@link TimeZone} is already instantiated */
    +    @SuppressWarnings("deprecation")
    +    public static String getTimeZoneOffsetString(TimeZone tz, int year, int month, int day) {
    +        int tzMins = tz.getOffset(new Date(year, month, day).getTime())/60/1000;
    +        String tzStr = (tzMins<0 ? "-" : "+") + Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", "")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", "");
    +        return tzStr;
    +    }
    +
    +    private static String namedGroup(String name, String pattern) {
    +        return "(?<"+name+">"+pattern+")";
    +    }
    +    private static String anyChar(String charSet) {
    +        return "["+charSet+"]";
    +    }
    +    private static String optionally(String pattern) {
    +        return "("+pattern+")?";
    +    }
    +    private static String options(String ...patterns) {
    +        return "("+Strings.join(patterns,"|")+")";
    +    }
    +    private static String notMatching(String pattern) {
    +        return "(?!"+pattern+")";
    +    }
    +    
    +    private static Maybe<Matcher> match(String pattern, String input) {
    +        Matcher m = Pattern.compile(pattern).matcher(input);
    +        if (m.find() && m.start()==0 && m.end()==input.length())
    +            return Maybe.of(m);
    +        return Maybe.absent();
    +    }
    +
    +//    // TODO https://github.com/joestelmach/natty is cool, but it drags in ANTLR,
    --- End diff --
    
    What is the TODO? To decide whether ANTLR is useful or not? Would prefer to either include Natty get rid of this commented block. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on a diff in the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#discussion_r32332801
  
    --- Diff: utils/common/src/main/java/brooklyn/util/time/Time.java ---
    @@ -443,36 +500,466 @@ else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreC
             }
         }
     
    +    public static Calendar newCalendarFromMillisSinceEpochUtc(long timestamp) {
    +        GregorianCalendar cal = new GregorianCalendar();
    +        cal.setTimeInMillis(timestamp);
    +        return cal;
    +    }
    +
    +    public static Calendar newCalendarFromDate(Date date) {
    +        return newCalendarFromMillisSinceEpochUtc(date.getTime());
    +    }
    +    
    +    /** As {@link #parseCalendar(String)} but returning a {@link Date},
    +     * (i.e. a record where the time zone has been applied and forgotten). */
    +    public static Date parseDate(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get().getTime();
    +    }
    +
    +    /** Parses dates from string, accepting many formats including ISO-8601 and http://yaml.org/type/timestamp.html, 
    +     * e.g. 2015-06-15 16:00:00 +0000.
    +     * <p>
    +     * Millis since epoch (1970) is also supported to represent the epoch (0) or dates in this millenium,
    +     * but to prevent ambiguity of e.g. "20150615", any other dates prior to the year 2001 are not accepted.
    +     * (However if a type Long is supplied, e.g. from a YAML parse, it will always be treated as millis since epoch.) 
    +     * <p>
    +     * Other formats including locale-specific variants, e.g. recognising month names,
    +     * are supported but this may vary from platform to platform and may change between versions. */
    +    public static Calendar parseCalendar(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get();
    +    }
    +    
    +    /** as {@link #parseCalendar(String)} but returning a {@link Maybe} rather than throwing or returning null */
    +    public static Maybe<Calendar> parseCalendarMaybe(String input) {
    +        if (input==null) return Maybe.absent("value is null");
    +        input = input.trim();
    +        Maybe<Calendar> result;
    +
    +        result = parseCalendarUtc(input);
    +        if (result.isPresent()) return result;
    +
    +        result = parseCalendarSimpleFlexibleFormatParser(input);
    +        if (result.isPresent()) return result;
    +        // return the error from this method
    +        Maybe<Calendar> returnResult = result;
    +
    +//        // see natty method comments below
    +//        Maybe<Date> result = parseDateNatty(input);
    +//        if (result.isPresent()) return result;
    +
    +        result = parseCalendarFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
    +        if (result.isPresent()) return result;
    +        result = parseCalendarDefaultParse(input);
    +        if (result.isPresent()) return result;
    +
    +        return returnResult;
    +    }
    +
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarDefaultParse(String input) {
    +        try {
    +            long ms = Date.parse(input);
    +            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
    +                // accept default date parse for this century and next
    +                GregorianCalendar c = new GregorianCalendar();
    +                c.setTimeInMillis(ms);
    +                return Maybe.of((Calendar)c);
    +            }
    +        } catch (Exception e) {
    +            Exceptions.propagateIfFatal(e);
    +        }
    +        return Maybe.absent();
    +    }
    +
    +    private static Maybe<Calendar> parseCalendarUtc(String input) {
    +        input = input.trim();
    +        if (input.matches("\\d+")) {
    +            if ("0".equals(input)) {
    +                // accept 0 as epoch UTC
    +                return Maybe.of(newCalendarFromMillisSinceEpochUtc(0));
    +            }
    +            Maybe<Calendar> result = Maybe.of(newCalendarFromMillisSinceEpochUtc(Long.parseLong(input)));
    +            if (result.isPresent()) {
    +                int year = result.get().get(Calendar.YEAR);
    +                if (year >= 2000 && year < 2200) {
    +                    // only applicable for dates in this century
    +                    return result;
    +                } else {
    +                    return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
    +                }
    +            }
    +        }
    +        return Maybe.absent("not long millis since epoch UTC");
    +    }
    +
    +    private final static String DIGIT = "\\d";
    +    private final static String LETTER = "\\p{L}";
    +    private final static String COMMON_SEPARATORS = "-\\.";
    +    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
    +    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
    +    private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
    +
    +    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+"))
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("day", optionally(DIGIT)+DIGIT);
    +    private final static String DATE_WORDS_2 = 
    +        namedGroup("month", anyChar(LETTER)+"+")
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
    +    private final static String DATE_WORDS_3 = 
    +        namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("month", anyChar(LETTER)+"+")
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +
    +    private final static String DATE_ONLY_NO_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + namedGroup("month", DIGIT+DIGIT)
    +            + namedGroup("day", DIGIT+DIGIT);
    +
    +    private final static String MERIDIAN = anyChar("aApP")+optionally(anyChar("mM"));
    +    private final static String TIME_ONLY_WITH_INNER_SEPARATORS = 
    +        namedGroup("hours", optionally(DIGIT)+DIGIT)+
    +        optionally(
    +            anyChar(TIME_SEPARATOR)+
    +            namedGroup("mins", DIGIT+DIGIT)+
    +            optionally(
    +                anyChar(TIME_SEPARATOR)+
    +                namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+"))))+
    +        optionally(" *" + namedGroup("meridian", notMatching(LETTER+LETTER+LETTER)+MERIDIAN));
    +    private final static String TIME_ONLY_NO_SEPARATORS = 
    +        namedGroup("hours", DIGIT+DIGIT)+
    +        namedGroup("mins", DIGIT+DIGIT)+
    +        optionally(
    +            namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+")))+
    +        namedGroup("meridian", "");
    +
    +    private final static String TZ_CODE = namedGroup("tzCode",
    +        notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) // not AM or PM
    +        + anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*");
    +    private final static String TIME_ZONE_SIGNED_OFFSET = namedGroup("tz", options(namedGroup("tzOffset", options("\\+", "-")+
    +            DIGIT+optionally(DIGIT)+optionally(optionally(":")+DIGIT+DIGIT)), 
    +        optionally("\\+")+TZ_CODE));
    +    private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET = namedGroup("tz", 
    +        options(
    +            namedGroup("tzOffset", options("\\+", "-", " ")+
    +                options("0"+DIGIT, "10", "11", "12")+optionally(optionally(":")+DIGIT+DIGIT)), 
    +            TZ_CODE));
    +
    +    private static String getDateTimeSeparatorPattern(String extraChars) {
    +        return options(" +"+optionally(anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")),
    +                anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+","))
    +            + anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars)+"*";
    +    }
    +    
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarSimpleFlexibleFormatParser(String input) {
    +        input = input.trim();
    +
    +        String[] DATE_PATTERNS = new String[] {
    +            DATE_ONLY_WITH_INNER_SEPARATORS,
    +            DATE_ONLY_NO_SEPARATORS,
    +            DATE_WORDS_2,
    +            DATE_WORDS_3,            
    +        };
    +        String[] TIME_PATTERNS = new String[] {
    +            TIME_ONLY_WITH_INNER_SEPARATORS,
    +            TIME_ONLY_NO_SEPARATORS            
    +        };
    +        String[] TZ_PATTERNS = new String[] {
    +            // space then time zone with sign (+-) or code is preferred
    +            optionally(getDateTimeSeparatorPattern("")) + " " + TIME_ZONE_SIGNED_OFFSET,
    +            // then no TZ - but declare the named groups
    +            namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", "")),
    +            // then any separator then offset with sign
    +            getDateTimeSeparatorPattern("") + TIME_ZONE_SIGNED_OFFSET,
    +            
    +            // try parsing with enforced separators before TZ first 
    +            // (so e.g. in the case of DATE-0100, the -0100 is the time, not the timezone)
    +            // then relax below (e.g. in the case of DATE-TIME+0100)
    +            
    +            // finally match DATE-TIME-1000 as time zone -1000
    +            // or DATE-TIME 1000 as TZ +1000 in case a + was supplied but converted to ' ' by web
    +            // (but be stricter about the format, two or four digits required, and hours <= 12 so as not to confuse with a year)
    +            optionally(getDateTimeSeparatorPattern("")) + TIME_ZONE_OPTIONALLY_SIGNED_OFFSET
    +        };
    +        
    +        List<String> basePatterns = MutableList.of();
    +        
    +        // patterns with date first
    +        String[] DATE_PATTERNS_UNCLOSED = new String[] {
    +            // separator before time *required* if date had separators
    +            DATE_ONLY_WITH_INNER_SEPARATORS + "("+getDateTimeSeparatorPattern("Tt"),
    +            // separator before time optional if date did not have separators
    +            DATE_ONLY_NO_SEPARATORS + "("+optionally(getDateTimeSeparatorPattern("Tt")),
    +            // separator before time required if date has words
    +            DATE_WORDS_2 + "("+getDateTimeSeparatorPattern("Tt"),
    +            DATE_WORDS_3 + "("+getDateTimeSeparatorPattern("Tt"),
    +        };
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS_UNCLOSED)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + dateP + timeP+")?" + tzP + "$");
    +        
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + getDateTimeSeparatorPattern("") + dateP + tzP + "$");
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + tzP + getDateTimeSeparatorPattern("") + dateP + "$");
    +
    +        Maybe<Matcher> mm = Maybe.absent();
    +        for (String p: basePatterns) {
    +            mm = match(p, input);
    +            if (mm.isPresent()) break;
    +        }
    +        if (mm.isPresent()) {
    +            Matcher m = mm.get();
    +            Calendar result;
    +
    +            String tz = m.group("tz");
    +            
    +            int year = Integer.parseInt(m.group("year"));
    +            int day = Integer.parseInt(m.group("day"));
    +            
    +            String monthS = m.group("month");
    +            int month;
    +            if (monthS.matches(DIGIT+"+")) {
    +                month = Integer.parseInt(monthS)-1;
    +            } else {
    +                try {
    +                    month = new SimpleDateFormat("yyyy-MMM-dd").parse("2015-"+monthS+"-15").getMonth();
    +                } catch (ParseException e) {
    +                    return Maybe.absent("Unknown date format '"+input+"': invalid month '"+monthS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                }
    +            }
    +            
    +            if (Strings.isNonBlank(tz)) {
    +                TimeZone tzz = null;
    +                String tzCode = m.group("tzCode");
    +                if (Strings.isNonBlank(tzCode)) {
    +                    tz = tzCode;
    +                }
    +                if (tz.matches(DIGIT+"+")) {
    +                    // stick a plus in front in case it was submitted by a web form and turned into a space
    +                    tz = "+"+tz;
    +                } else {
    +                    tzz = getTimeZone(tz);
    +                }
    +                if (tzz==null) {
    +                    Maybe<Matcher> tmm = match("^ ?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")"+optionally(optionally(":")+namedGroup("tzM", DIGIT+DIGIT))+"$", tz);
    +                    if (tmm.isAbsent()) {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid timezone '"+tz+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    Matcher tm = tmm.get();
    +                    String tzM = tm.group("tzM");
    +                    int offset = (60*Integer.parseInt(tm.group("tzH")) + Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60;
    +                    tzz = new SimpleTimeZone(offset*1000, tz);
    +                }
    +                tz = getTimeZoneOffsetString(tzz, year, month, day);
    +                result = new GregorianCalendar(tzz);
    +            } else {
    +                result = new GregorianCalendar();
    +            }
    +            result.clear();
    +            
    +            result.set(Calendar.YEAR, year);
    +            result.set(Calendar.MONTH, month);
    +            result.set(Calendar.DAY_OF_MONTH, day);
    +            if (m.group("hours")!=null) {
    +                int hours = Integer.parseInt(m.group("hours"));
    +                String meridian = m.group("meridian");
    +                if (Strings.isNonBlank(meridian) && meridian.toLowerCase().startsWith("p")) {
    +                    if (hours>12) {
    +                        return Maybe.absent("Unknown date format '"+input+"': can't be "+hours+" PM; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    hours += 12;
    +                }
    +                result.set(Calendar.HOUR_OF_DAY, hours);
    +                String minsS = m.group("mins");
    +                if (Strings.isNonBlank(minsS)) {
    +                    result.set(Calendar.MINUTE, Integer.parseInt(minsS));
    +                }
    +                String secsS = m.group("secs");
    +                if (Strings.isBlank(secsS)) {
    +                    // leave at zero
    +                } else if (secsS.matches(DIGIT+DIGIT+"?")) {
    +                    result.set(Calendar.SECOND, Integer.parseInt(secsS));
    +                } else {
    +                    double s = Double.parseDouble(secsS);
    +                    if (s>=0 && s<=60) {
    +                        // in double format, with correct period
    +                    } else if (secsS.length()==5) {
    +                        // allow ssSSS with no punctuation
    +                        s = s/=1000;
    +                    } else {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid seconds '"+secsS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    result.set(Calendar.SECOND, (int)s);
    +                    result.set(Calendar.MILLISECOND, (int)((s*1000) % 1000));
    +                }
    +            }
    +            
    +            return Maybe.of(result);
    +        }
    +        return Maybe.absent("Unknown date format '"+input+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +    }
    +    
    +    public static TimeZone getTimeZone(String code) {
    +        if (code.indexOf('/')==-1) {
    +            if ("Z".equals(code)) return TIME_ZONE_UTC;
    +            if ("UTC".equals(code)) return TIME_ZONE_UTC;
    +            if ("GMT".equals(code)) return TIME_ZONE_UTC;
    +            
    +            // get the time zone -- most short codes aren't accepted, so accept (and prefer) certain common codes
    +            if ("EST".equals(code)) return getTimeZone("America/New_York");
    +            if ("EDT".equals(code)) return getTimeZone("America/New_York");
    +            if ("PST".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("CST".equals(code)) return getTimeZone("America/Chicago");
    +            if ("CDT".equals(code)) return getTimeZone("America/Chicago");
    +            if ("MST".equals(code)) return getTimeZone("America/Denver");
    +            if ("MDT".equals(code)) return getTimeZone("America/Denver");
    +
    +            if ("BST".equals(code)) return getTimeZone("Europe/London");  // otherwise BST is Bangladesh!
    +            if ("CEST".equals(code)) return getTimeZone("Europe/Paris");
    +            // IST falls through to below, where it is treated as India (not Irish); IDT not recognised
    +        }
    +        
    +        TimeZone tz = TimeZone.getTimeZone(code);
    +        if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) {
    +            // recognized
    +            return tz;
    +        }
    +        // possibly unrecognized -- GMT returned if not known, bad TimeZone API!
    +        String timeZones[] = TimeZone.getAvailableIDs();
    +        for (String tzs: timeZones) {
    +            if (tzs.equals(code)) return tz;
    +        }
    +        // definitely unrecognized
    +        return null;
    +    }
    +    
    +    /** convert a TimeZone e.g. Europe/London to an offset string as at the given day, e.g. +0100 or +0000 depending daylight savings,
    +     * absent with nice error if zone unknown */
    +    public static Maybe<String> getTimeZoneOffsetString(String tz, int year, int month, int day) {
    +        TimeZone tzz = getTimeZone(tz);
    +        if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz);
    +        return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day));
    +    }
    +    
    +    /** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the {@link TimeZone} is already instantiated */
    +    @SuppressWarnings("deprecation")
    +    public static String getTimeZoneOffsetString(TimeZone tz, int year, int month, int day) {
    +        int tzMins = tz.getOffset(new Date(year, month, day).getTime())/60/1000;
    +        String tzStr = (tzMins<0 ? "-" : "+") + Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", "")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", "");
    +        return tzStr;
    +    }
    +
    +    private static String namedGroup(String name, String pattern) {
    +        return "(?<"+name+">"+pattern+")";
    +    }
    +    private static String anyChar(String charSet) {
    +        return "["+charSet+"]";
    +    }
    +    private static String optionally(String pattern) {
    +        return "("+pattern+")?";
    +    }
    +    private static String options(String ...patterns) {
    +        return "("+Strings.join(patterns,"|")+")";
    +    }
    +    private static String notMatching(String pattern) {
    +        return "(?!"+pattern+")";
    +    }
    +    
    +    private static Maybe<Matcher> match(String pattern, String input) {
    +        Matcher m = Pattern.compile(pattern).matcher(input);
    +        if (m.find() && m.start()==0 && m.end()==input.length())
    +            return Maybe.of(m);
    +        return Maybe.absent();
    +    }
    +
    +//    // TODO https://github.com/joestelmach/natty is cool, but it drags in ANTLR,
    --- End diff --
    
    +1


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-110040537
  
    This PR supports that -- except for lower-case `t` which I'll add.
    
    The YAML standard is a good one to recommend -- I'll add that to docs and error messages.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on a diff in the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#discussion_r31907388
  
    --- Diff: utils/common/src/main/java/brooklyn/util/time/Time.java ---
    @@ -443,36 +472,433 @@ else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreC
             }
         }
     
    +    /** parses dates from string, accepting many formats including 'YYYY-MM-DD', 'YYYY-MM-DD HH:mm:SS', or millis since UTC epoch */
    +    public static Date parseDate(String input) {
    +        if (input==null) return null;
    +        return parseDateMaybe(input).get();
    +    }
    +    
    +    /** as {@link #parseDate(String)} but returning a {@link Maybe} rather than throwing or returning null */
    +    public static Maybe<Date> parseDateMaybe(String input) {
    +        if (input==null) return Maybe.absent("value is null");
    +        input = input.trim();
    +        Maybe<Date> result;
    +
    +        result = parseDateUtc(input);
    +        if (result.isPresent()) return result;
    +
    +        result = parseDateSimpleFlexibleFormatParser(input);
    +        if (result.isPresent()) return result;
    +        // return the error from this method
    +        Maybe<Date> returnResult = result;
    +
    +//        // see natty method comments below
    +//        Maybe<Date> result = parseDateNatty(input);
    +//        if (result.isPresent()) return result;
    +
    +        result = parseDateFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
    +        if (result.isPresent()) return result;
    +        result = parseDateDefaultParse(input);
    +        if (result.isPresent()) return result;
    +
    +        return returnResult;
    +    }
    +
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Date> parseDateDefaultParse(String input) {
    +        try {
    +            long ms = Date.parse(input);
    +            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
    +                // accept default date parse for this century and next
    +                return Maybe.of(new Date(ms));
    +            }
    +        } catch (Exception e) {
    +            Exceptions.propagateIfFatal(e);
    +        }
    +        return Maybe.absent();
    +    }
    +
    +    private static Maybe<Date> parseDateUtc(String input) {
    +        if (input.matches("\\d+")) {
    +            Maybe<Date> result = Maybe.of(new Date(Long.parseLong(input)));
    +            if (result.isPresent()) {
    +                @SuppressWarnings("deprecation")
    +                int year = result.get().getYear();
    +                if (year >= 2000 && year < 2200) {
    +                    // only applicable for dates in this century
    +                    return result;
    +                } else {
    +                    return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
    +                }
    +            }
    +        }
    +        return Maybe.absent("not long millis since epoch UTC");
    +    }
    +
    +    private final static String DIGIT = "\\d";
    +    private final static String LETTER = "\\p{L}";
    +    private final static String COMMON_SEPARATORS = "-\\.";
    +    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
    +    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
    +    private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
    +
    +    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+"))
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("day", optionally(DIGIT)+DIGIT);
    +    private final static String DATE_WORDS_2 = 
    +        namedGroup("month", anyChar(LETTER)+"+")
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
    +    private final static String DATE_WORDS_3 = 
    +        namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("month", anyChar(LETTER)+"+")
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +
    +    private final static String DATE_ONLY_NO_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + namedGroup("month", DIGIT+DIGIT)
    +            + namedGroup("day", DIGIT+DIGIT);
    +
    +    private final static String MERIDIAN = anyChar("aApP")+optionally(anyChar("mM"));
    +    private final static String TIME_ONLY_WITH_INNER_SEPARATORS = 
    +        namedGroup("hours", optionally(DIGIT)+DIGIT)+
    +        optionally(
    +            anyChar(TIME_SEPARATOR)+
    +            namedGroup("mins", DIGIT+DIGIT)+
    +            optionally(
    +                anyChar(TIME_SEPARATOR)+
    +                namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+"))))+
    +        optionally(" *" + namedGroup("meridian", notMatching(LETTER+LETTER+LETTER)+MERIDIAN));
    +    private final static String TIME_ONLY_NO_SEPARATORS = 
    +        namedGroup("hours", DIGIT+DIGIT)+
    +        namedGroup("mins", DIGIT+DIGIT)+
    +        optionally(
    +            namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+")))+
    +        namedGroup("meridian", "");
    +
    +    private final static String TZ_CODE = namedGroup("tzCode",
    +        notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) // not AM or PM
    +        + anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*");
    +    private final static String TIME_ZONE_SIGNED_OFFSET = namedGroup("tz", options(namedGroup("tzOffset", options("\\+", "-")+
    +            DIGIT+optionally(DIGIT)+optionally(optionally(":")+DIGIT+DIGIT)), 
    +        optionally("\\+")+TZ_CODE));
    +    private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET = namedGroup("tz", 
    +        options(
    +            namedGroup("tzOffset", options("\\+", "-", " ")+
    +                options("0"+DIGIT, "10", "11", "12")+optionally(optionally(":")+DIGIT+DIGIT)), 
    +            TZ_CODE));
    +
    +    private static String getDateTimeSeparatorPattern(String extraChars) {
    +        return options(" +"+optionally(anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")),
    +                anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+","))
    +            + anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars)+"*";
    +    }
    +    
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Date> parseDateSimpleFlexibleFormatParser(String input) {
    +        input = input.trim();
    +
    +        String[] DATE_PATTERNS = new String[] {
    +            DATE_ONLY_WITH_INNER_SEPARATORS,
    +            DATE_ONLY_NO_SEPARATORS,
    +            DATE_WORDS_2,
    +            DATE_WORDS_3,            
    +        };
    +        String[] TIME_PATTERNS = new String[] {
    +            TIME_ONLY_WITH_INNER_SEPARATORS,
    +            TIME_ONLY_NO_SEPARATORS            
    +        };
    +        String[] TZ_PATTERNS = new String[] {
    +            // space then time zone with sign (+-) or code is preferred
    +            optionally(getDateTimeSeparatorPattern("")) + " " + TIME_ZONE_SIGNED_OFFSET,
    +            // then no TZ - but declare the named groups
    +            namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", "")),
    +            // then any separator then offset with sign
    +            getDateTimeSeparatorPattern("") + TIME_ZONE_SIGNED_OFFSET,
    +            
    +            // try parsing with enforced separators before TZ first 
    +            // (so e.g. in the case of DATE-0100, the -0100 is the time, not the timezone)
    +            // then relax below (e.g. in the case of DATE-TIME+0100)
    +            
    +            // finally match DATE-TIME-1000 as time zone -1000
    +            // or DATE-TIME 1000 as TZ +1000 in case a + was supplied but converted to ' ' by web
    +            // (but be stricter about the format, two or four digits required, and hours <= 12 so as not to confuse with a year)
    +            optionally(getDateTimeSeparatorPattern("")) + TIME_ZONE_OPTIONALLY_SIGNED_OFFSET
    +        };
    +        
    +        List<String> basePatterns = MutableList.of();
    +        
    +        // patterns with date first
    +        String[] DATE_PATTERNS_UNCLOSED = new String[] {
    +            // separator before time *required* if date had separators
    +            DATE_ONLY_WITH_INNER_SEPARATORS + "("+getDateTimeSeparatorPattern("T"),
    +            // separator before time optional if date did not have separators
    +            DATE_ONLY_NO_SEPARATORS + "("+optionally(getDateTimeSeparatorPattern("T")),
    +            // separator before time required if date has words
    +            DATE_WORDS_2 + "("+getDateTimeSeparatorPattern("T"),
    +            DATE_WORDS_3 + "("+getDateTimeSeparatorPattern("T"),
    +        };
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS_UNCLOSED)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + dateP + timeP+")?" + tzP + "$");
    +        
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + getDateTimeSeparatorPattern("") + dateP + tzP + "$");
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + tzP + getDateTimeSeparatorPattern("") + dateP + "$");
    +
    +        Maybe<Matcher> mm = Maybe.absent();
    +        for (String p: basePatterns) {
    +            mm = match(p, input);
    +            if (mm.isPresent()) break;
    +        }
    +        if (mm.isPresent()) {
    +            Matcher m = mm.get();
    +            Calendar result;
    +
    +            String tz = m.group("tz");
    +            
    +            int year = Integer.parseInt(m.group("year"));
    +            int day = Integer.parseInt(m.group("day"));
    +            
    +            String monthS = m.group("month");
    +            int month;
    +            if (monthS.matches(DIGIT+"+")) {
    +                month = Integer.parseInt(monthS)-1;
    +            } else {
    +                try {
    +                    month = new SimpleDateFormat("yyyy-MMM-dd").parse("2015-"+monthS+"-15").getMonth();
    +                } catch (ParseException e) {
    +                    return Maybe.absent("Unknown date format '"+input+"': invalid month '"+monthS+"'; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
    +                }
    +            }
    +            
    +            if (Strings.isNonBlank(tz)) {
    +                TimeZone tzz = null;
    +                String tzCode = m.group("tzCode");
    +                if (Strings.isNonBlank(tzCode)) {
    +                    tz = tzCode;
    +                }
    +                if (tz.matches(DIGIT+"+")) {
    +                    // stick a plus in front in case it was submitted by a web form and turned into a space
    +                    tz = "+"+tz;
    +                } else {
    +                    tzz = getTimeZone(tz);
    +                }
    +                if (tzz==null) {
    +                    Maybe<Matcher> tmm = match("^ ?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")"+optionally(optionally(":")+namedGroup("tzM", DIGIT+DIGIT))+"$", tz);
    +                    if (tmm.isAbsent()) {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid timezone '"+tz+"'; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
    +                    }
    +                    Matcher tm = tmm.get();
    +                    String tzM = tm.group("tzM");
    +                    int offset = (60*Integer.parseInt(tm.group("tzH")) + Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60;
    +                    tzz = new SimpleTimeZone(offset*1000, tz);
    +                }
    +                tz = getTimeZoneOffsetString(tzz, year, month, day);
    +                result = new GregorianCalendar(tzz);
    +            } else {
    +                result = new GregorianCalendar();
    +            }
    +            result.clear();
    +            
    +            result.set(Calendar.YEAR, year);
    +            result.set(Calendar.MONTH, month);
    +            result.set(Calendar.DAY_OF_MONTH, day);
    +            if (m.group("hours")!=null) {
    +                int hours = Integer.parseInt(m.group("hours"));
    +                String meridian = m.group("meridian");
    +                if (Strings.isNonBlank(meridian) && meridian.toLowerCase().startsWith("p")) {
    +                    if (hours>12) {
    +                        return Maybe.absent("Unknown date format '"+input+"': can't be "+hours+" PM; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
    +                    }
    +                    hours += 12;
    +                }
    +                result.set(Calendar.HOUR_OF_DAY, hours);
    +                String minsS = m.group("mins");
    +                if (Strings.isNonBlank(minsS)) {
    +                    result.set(Calendar.MINUTE, Integer.parseInt(minsS));
    +                }
    +                String secsS = m.group("secs");
    +                if (Strings.isBlank(secsS)) {
    +                    // leave at zero
    +                } else if (secsS.matches(DIGIT+DIGIT+"?")) {
    +                    result.set(Calendar.SECOND, Integer.parseInt(secsS));
    +                } else {
    +                    double s = Double.parseDouble(secsS);
    +                    if (s>=0 && s<=60) {
    +                        // in double format, with correct period
    +                    } else if (secsS.length()==5) {
    +                        // allow ssSSS with no punctuation
    +                        s = s/=1000;
    +                    } else {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid seconds '"+secsS+"'; try 'YYYY-MM-DD HH:mm:ss.SSS +0000'");
    +                    }
    +                    result.set(Calendar.SECOND, (int)s);
    +                    result.set(Calendar.MILLISECOND, (int)((s*1000) % 1000));
    +                }
    +            }
    +            
    +            return Maybe.of(result.getTime());
    +        }
    +        return Maybe.absent("Unknown date format '"+input+"'; try ISO-8601, or 'yyyy-MM-dd' or 'yyyy-MM-dd HH:mm:ss +0000'");
    +    }
    +    
    +    public static TimeZone getTimeZone(String code) {
    +        if (code.indexOf('/')==-1) {
    +            if ("Z".equals(code)) return getTimeZone("UTC");
    +            
    +            // get the time zone -- most short codes aren't accepted, so accept (and prefer) certain common codes
    +            if ("EST".equals(code)) return getTimeZone("America/New_York");
    +            if ("EDT".equals(code)) return getTimeZone("America/New_York");
    +            if ("PST".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("CST".equals(code)) return getTimeZone("America/Chicago");
    +            if ("CDT".equals(code)) return getTimeZone("America/Chicago");
    +            if ("MST".equals(code)) return getTimeZone("America/Denver");
    +            if ("MDT".equals(code)) return getTimeZone("America/Denver");
    +
    +            if ("BST".equals(code)) return getTimeZone("Europe/London");  // otherwise BST is Bangladesh!
    +            if ("CEST".equals(code)) return getTimeZone("Europe/Paris");
    +            // IST falls through to below, where it is treated as India (not Irish); IDT not recognised
    +        }
    +        
    +        TimeZone tz = TimeZone.getTimeZone(code);
    +        if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) {
    +            // recognized
    +            return tz;
    +        }
    +        // possibly unrecognized -- GMT returned if not known, bad TimeZone API!
    +        String timeZones[] = TimeZone.getAvailableIDs();
    +        for (String tzs: timeZones) {
    +            if (tzs.equals(code)) return tz;
    +        }
    +        // definitely unrecognized
    +        return null;
    +    }
    +    
    +    /** convert a TimeZone e.g. Europe/London to an offset string as at the given day, e.g. +0100 or +0000 depending daylight savings,
    +     * absent with nice error if zone unknown */
    +    public static Maybe<String> getTimeZoneOffsetString(String tz, int year, int month, int day) {
    +        TimeZone tzz = getTimeZone(tz);
    +        if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz);
    +        return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day));
    +    }
    +    
    +    /** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the {@link TimeZone} is already instantiated */
    +    @SuppressWarnings("deprecation")
    +    public static String getTimeZoneOffsetString(TimeZone tz, int year, int month, int day) {
    +        int tzMins = tz.getOffset(new Date(year, month, day).getTime())/60/1000;
    +        String tzStr = (tzMins<0 ? "-" : "+") + Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", "")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", "");
    +        return tzStr;
    +    }
    +
    +    private static String namedGroup(String name, String pattern) {
    +        return "(?<"+name+">"+pattern+")";
    +    }
    +    private static String anyChar(String charSet) {
    +        return "["+charSet+"]";
    +    }
    +    private static String optionally(String pattern) {
    +        return "("+pattern+")?";
    --- End diff --
    
    It would be permitted but not sure this optimization is worth the add'l regex complexity.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-110857970
  
    yay - bumping our builds.apache.org to java 1.7 and the PR works.
    
    we'll need to do the same for other builds before this is merged.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-110051989
  
    Updated to recommend the YAML standard, although in practice it supports more, including most of the obvious unambiguous formats.
    
    Also fleshed out docs esp on `TypeCoercions.coerce` which we can start to point to as the canonical way to represent non-custom types from yaml.
    
    (Still need to flip jenkins to java7.)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-110852517
  
    @sjcorbett what?  a user here can use any sensible format and get the expected result without having to look anything up.  do you disagree?  or do you think that's a bad thing?
    
    the point you make is a different one, that to understand really perverse things you'd need to look at the implementation, and i think that's okay.  (although actually here they could just look at the test cases or try it out or infer from the javadoc which recommends date first that date-first is probably preferred.)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by sjcorbett <gi...@git.apache.org>.
Github user sjcorbett commented on a diff in the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#discussion_r32301606
  
    --- Diff: utils/common/src/main/java/brooklyn/util/time/Time.java ---
    @@ -443,36 +500,466 @@ else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreC
             }
         }
     
    +    public static Calendar newCalendarFromMillisSinceEpochUtc(long timestamp) {
    +        GregorianCalendar cal = new GregorianCalendar();
    +        cal.setTimeInMillis(timestamp);
    +        return cal;
    +    }
    +
    +    public static Calendar newCalendarFromDate(Date date) {
    +        return newCalendarFromMillisSinceEpochUtc(date.getTime());
    +    }
    +    
    +    /** As {@link #parseCalendar(String)} but returning a {@link Date},
    +     * (i.e. a record where the time zone has been applied and forgotten). */
    +    public static Date parseDate(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get().getTime();
    +    }
    +
    +    /** Parses dates from string, accepting many formats including ISO-8601 and http://yaml.org/type/timestamp.html, 
    +     * e.g. 2015-06-15 16:00:00 +0000.
    +     * <p>
    +     * Millis since epoch (1970) is also supported to represent the epoch (0) or dates in this millenium,
    +     * but to prevent ambiguity of e.g. "20150615", any other dates prior to the year 2001 are not accepted.
    +     * (However if a type Long is supplied, e.g. from a YAML parse, it will always be treated as millis since epoch.) 
    +     * <p>
    +     * Other formats including locale-specific variants, e.g. recognising month names,
    +     * are supported but this may vary from platform to platform and may change between versions. */
    +    public static Calendar parseCalendar(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get();
    +    }
    +    
    +    /** as {@link #parseCalendar(String)} but returning a {@link Maybe} rather than throwing or returning null */
    +    public static Maybe<Calendar> parseCalendarMaybe(String input) {
    +        if (input==null) return Maybe.absent("value is null");
    +        input = input.trim();
    +        Maybe<Calendar> result;
    +
    +        result = parseCalendarUtc(input);
    +        if (result.isPresent()) return result;
    +
    +        result = parseCalendarSimpleFlexibleFormatParser(input);
    +        if (result.isPresent()) return result;
    +        // return the error from this method
    +        Maybe<Calendar> returnResult = result;
    +
    +//        // see natty method comments below
    +//        Maybe<Date> result = parseDateNatty(input);
    +//        if (result.isPresent()) return result;
    +
    +        result = parseCalendarFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
    +        if (result.isPresent()) return result;
    +        result = parseCalendarDefaultParse(input);
    +        if (result.isPresent()) return result;
    +
    +        return returnResult;
    +    }
    +
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarDefaultParse(String input) {
    +        try {
    +            long ms = Date.parse(input);
    +            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
    +                // accept default date parse for this century and next
    +                GregorianCalendar c = new GregorianCalendar();
    +                c.setTimeInMillis(ms);
    +                return Maybe.of((Calendar)c);
    +            }
    +        } catch (Exception e) {
    +            Exceptions.propagateIfFatal(e);
    +        }
    +        return Maybe.absent();
    +    }
    +
    +    private static Maybe<Calendar> parseCalendarUtc(String input) {
    +        input = input.trim();
    +        if (input.matches("\\d+")) {
    +            if ("0".equals(input)) {
    +                // accept 0 as epoch UTC
    +                return Maybe.of(newCalendarFromMillisSinceEpochUtc(0));
    +            }
    +            Maybe<Calendar> result = Maybe.of(newCalendarFromMillisSinceEpochUtc(Long.parseLong(input)));
    +            if (result.isPresent()) {
    +                int year = result.get().get(Calendar.YEAR);
    +                if (year >= 2000 && year < 2200) {
    +                    // only applicable for dates in this century
    +                    return result;
    +                } else {
    +                    return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
    +                }
    +            }
    +        }
    +        return Maybe.absent("not long millis since epoch UTC");
    +    }
    +
    +    private final static String DIGIT = "\\d";
    +    private final static String LETTER = "\\p{L}";
    +    private final static String COMMON_SEPARATORS = "-\\.";
    +    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
    +    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
    +    private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
    +
    +    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+"))
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("day", optionally(DIGIT)+DIGIT);
    +    private final static String DATE_WORDS_2 = 
    +        namedGroup("month", anyChar(LETTER)+"+")
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
    +    private final static String DATE_WORDS_3 = 
    +        namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("month", anyChar(LETTER)+"+")
    --- End diff --
    
    This formatting (starting the line with a +) is a bit weird to me. It's a bit inconsistent between patterns at the moment, too (e.g. `TIME_ONLY_WITH_INNER_SEPARATORS` is at the end of the line).


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by sjcorbett <gi...@git.apache.org>.
Github user sjcorbett commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-109931239
  
    The regular expressions in `Time` make me wary. Why write date parsing yourself when you could just use joda-time? It's already solved these things for us. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by alasdairhodge <gi...@git.apache.org>.
Github user alasdairhodge commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-109981181
  
    Well I strongly disagree that a single (ubiquitous and standards-defined) format makes things "harder to work with" — just the opposite, actually! ISO's ubiquity easily makes up for it's wrinkles.
    
    I my view, our primary focus should be on an unambiguous, interoperable representation with "friendly" aesthetics a secondary concern. If things like field delimiters are a big issue for users (really?!) then arguably we should simply defer to the locale to do the parsing according to the user's whims (although this will open a huge can of worms when blueprints are shared).


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-109908674
  
    this uses named groups in regexes which is only avail in java 7+.
    
    i'm going to suggest we raise the minimum required version to 7 -- also implemented in this PR.  title updated to reflect this.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-109967073
  
    Re regex conveniences -- these methods aren't strictly fluent; a fluent API for regexes would be lovely but a much bigger task than the few conveniences I've put here.  But thanks for the compliment!


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin closed the pull request at:

    https://github.com/apache/incubator-brooklyn/pull/682


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on a diff in the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#discussion_r32332822
  
    --- Diff: utils/common/src/main/java/brooklyn/util/time/Time.java ---
    @@ -443,36 +500,466 @@ else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreC
             }
         }
     
    +    public static Calendar newCalendarFromMillisSinceEpochUtc(long timestamp) {
    +        GregorianCalendar cal = new GregorianCalendar();
    +        cal.setTimeInMillis(timestamp);
    +        return cal;
    +    }
    +
    +    public static Calendar newCalendarFromDate(Date date) {
    +        return newCalendarFromMillisSinceEpochUtc(date.getTime());
    +    }
    +    
    +    /** As {@link #parseCalendar(String)} but returning a {@link Date},
    +     * (i.e. a record where the time zone has been applied and forgotten). */
    +    public static Date parseDate(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get().getTime();
    +    }
    +
    +    /** Parses dates from string, accepting many formats including ISO-8601 and http://yaml.org/type/timestamp.html, 
    +     * e.g. 2015-06-15 16:00:00 +0000.
    +     * <p>
    +     * Millis since epoch (1970) is also supported to represent the epoch (0) or dates in this millenium,
    +     * but to prevent ambiguity of e.g. "20150615", any other dates prior to the year 2001 are not accepted.
    +     * (However if a type Long is supplied, e.g. from a YAML parse, it will always be treated as millis since epoch.) 
    +     * <p>
    +     * Other formats including locale-specific variants, e.g. recognising month names,
    +     * are supported but this may vary from platform to platform and may change between versions. */
    +    public static Calendar parseCalendar(String input) {
    +        if (input==null) return null;
    +        return parseCalendarMaybe(input).get();
    +    }
    +    
    +    /** as {@link #parseCalendar(String)} but returning a {@link Maybe} rather than throwing or returning null */
    +    public static Maybe<Calendar> parseCalendarMaybe(String input) {
    +        if (input==null) return Maybe.absent("value is null");
    +        input = input.trim();
    +        Maybe<Calendar> result;
    +
    +        result = parseCalendarUtc(input);
    +        if (result.isPresent()) return result;
    +
    +        result = parseCalendarSimpleFlexibleFormatParser(input);
    +        if (result.isPresent()) return result;
    +        // return the error from this method
    +        Maybe<Calendar> returnResult = result;
    +
    +//        // see natty method comments below
    +//        Maybe<Date> result = parseDateNatty(input);
    +//        if (result.isPresent()) return result;
    +
    +        result = parseCalendarFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
    +        if (result.isPresent()) return result;
    +        result = parseCalendarDefaultParse(input);
    +        if (result.isPresent()) return result;
    +
    +        return returnResult;
    +    }
    +
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarDefaultParse(String input) {
    +        try {
    +            long ms = Date.parse(input);
    +            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
    +                // accept default date parse for this century and next
    +                GregorianCalendar c = new GregorianCalendar();
    +                c.setTimeInMillis(ms);
    +                return Maybe.of((Calendar)c);
    +            }
    +        } catch (Exception e) {
    +            Exceptions.propagateIfFatal(e);
    +        }
    +        return Maybe.absent();
    +    }
    +
    +    private static Maybe<Calendar> parseCalendarUtc(String input) {
    +        input = input.trim();
    +        if (input.matches("\\d+")) {
    +            if ("0".equals(input)) {
    +                // accept 0 as epoch UTC
    +                return Maybe.of(newCalendarFromMillisSinceEpochUtc(0));
    +            }
    +            Maybe<Calendar> result = Maybe.of(newCalendarFromMillisSinceEpochUtc(Long.parseLong(input)));
    +            if (result.isPresent()) {
    +                int year = result.get().get(Calendar.YEAR);
    +                if (year >= 2000 && year < 2200) {
    +                    // only applicable for dates in this century
    +                    return result;
    +                } else {
    +                    return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
    +                }
    +            }
    +        }
    +        return Maybe.absent("not long millis since epoch UTC");
    +    }
    +
    +    private final static String DIGIT = "\\d";
    +    private final static String LETTER = "\\p{L}";
    +    private final static String COMMON_SEPARATORS = "-\\.";
    +    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
    +    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
    +    private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
    +
    +    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+"))
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("day", optionally(DIGIT)+DIGIT);
    +    private final static String DATE_WORDS_2 = 
    +        namedGroup("month", anyChar(LETTER)+"+")
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
    +    private final static String DATE_WORDS_3 = 
    +        namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("month", anyChar(LETTER)+"+")
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +
    +    private final static String DATE_ONLY_NO_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + namedGroup("month", DIGIT+DIGIT)
    +            + namedGroup("day", DIGIT+DIGIT);
    +
    +    private final static String MERIDIAN = anyChar("aApP")+optionally(anyChar("mM"));
    +    private final static String TIME_ONLY_WITH_INNER_SEPARATORS = 
    +        namedGroup("hours", optionally(DIGIT)+DIGIT)+
    +        optionally(
    +            anyChar(TIME_SEPARATOR)+
    +            namedGroup("mins", DIGIT+DIGIT)+
    +            optionally(
    +                anyChar(TIME_SEPARATOR)+
    +                namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+"))))+
    +        optionally(" *" + namedGroup("meridian", notMatching(LETTER+LETTER+LETTER)+MERIDIAN));
    +    private final static String TIME_ONLY_NO_SEPARATORS = 
    +        namedGroup("hours", DIGIT+DIGIT)+
    +        namedGroup("mins", DIGIT+DIGIT)+
    +        optionally(
    +            namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+")))+
    +        namedGroup("meridian", "");
    +
    +    private final static String TZ_CODE = namedGroup("tzCode",
    +        notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) // not AM or PM
    +        + anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*");
    +    private final static String TIME_ZONE_SIGNED_OFFSET = namedGroup("tz", options(namedGroup("tzOffset", options("\\+", "-")+
    +            DIGIT+optionally(DIGIT)+optionally(optionally(":")+DIGIT+DIGIT)), 
    +        optionally("\\+")+TZ_CODE));
    +    private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET = namedGroup("tz", 
    +        options(
    +            namedGroup("tzOffset", options("\\+", "-", " ")+
    +                options("0"+DIGIT, "10", "11", "12")+optionally(optionally(":")+DIGIT+DIGIT)), 
    +            TZ_CODE));
    +
    +    private static String getDateTimeSeparatorPattern(String extraChars) {
    +        return options(" +"+optionally(anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")),
    +                anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+","))
    +            + anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars)+"*";
    +    }
    +    
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Calendar> parseCalendarSimpleFlexibleFormatParser(String input) {
    +        input = input.trim();
    +
    +        String[] DATE_PATTERNS = new String[] {
    +            DATE_ONLY_WITH_INNER_SEPARATORS,
    +            DATE_ONLY_NO_SEPARATORS,
    +            DATE_WORDS_2,
    +            DATE_WORDS_3,            
    +        };
    +        String[] TIME_PATTERNS = new String[] {
    +            TIME_ONLY_WITH_INNER_SEPARATORS,
    +            TIME_ONLY_NO_SEPARATORS            
    +        };
    +        String[] TZ_PATTERNS = new String[] {
    +            // space then time zone with sign (+-) or code is preferred
    +            optionally(getDateTimeSeparatorPattern("")) + " " + TIME_ZONE_SIGNED_OFFSET,
    +            // then no TZ - but declare the named groups
    +            namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", "")),
    +            // then any separator then offset with sign
    +            getDateTimeSeparatorPattern("") + TIME_ZONE_SIGNED_OFFSET,
    +            
    +            // try parsing with enforced separators before TZ first 
    +            // (so e.g. in the case of DATE-0100, the -0100 is the time, not the timezone)
    +            // then relax below (e.g. in the case of DATE-TIME+0100)
    +            
    +            // finally match DATE-TIME-1000 as time zone -1000
    +            // or DATE-TIME 1000 as TZ +1000 in case a + was supplied but converted to ' ' by web
    +            // (but be stricter about the format, two or four digits required, and hours <= 12 so as not to confuse with a year)
    +            optionally(getDateTimeSeparatorPattern("")) + TIME_ZONE_OPTIONALLY_SIGNED_OFFSET
    +        };
    +        
    +        List<String> basePatterns = MutableList.of();
    +        
    +        // patterns with date first
    +        String[] DATE_PATTERNS_UNCLOSED = new String[] {
    +            // separator before time *required* if date had separators
    +            DATE_ONLY_WITH_INNER_SEPARATORS + "("+getDateTimeSeparatorPattern("Tt"),
    +            // separator before time optional if date did not have separators
    +            DATE_ONLY_NO_SEPARATORS + "("+optionally(getDateTimeSeparatorPattern("Tt")),
    +            // separator before time required if date has words
    +            DATE_WORDS_2 + "("+getDateTimeSeparatorPattern("Tt"),
    +            DATE_WORDS_3 + "("+getDateTimeSeparatorPattern("Tt"),
    +        };
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS_UNCLOSED)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + dateP + timeP+")?" + tzP + "$");
    +        
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + getDateTimeSeparatorPattern("") + dateP + tzP + "$");
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + tzP + getDateTimeSeparatorPattern("") + dateP + "$");
    +
    +        Maybe<Matcher> mm = Maybe.absent();
    +        for (String p: basePatterns) {
    +            mm = match(p, input);
    +            if (mm.isPresent()) break;
    +        }
    +        if (mm.isPresent()) {
    +            Matcher m = mm.get();
    +            Calendar result;
    +
    +            String tz = m.group("tz");
    +            
    +            int year = Integer.parseInt(m.group("year"));
    +            int day = Integer.parseInt(m.group("day"));
    +            
    +            String monthS = m.group("month");
    +            int month;
    +            if (monthS.matches(DIGIT+"+")) {
    +                month = Integer.parseInt(monthS)-1;
    +            } else {
    +                try {
    +                    month = new SimpleDateFormat("yyyy-MMM-dd").parse("2015-"+monthS+"-15").getMonth();
    +                } catch (ParseException e) {
    +                    return Maybe.absent("Unknown date format '"+input+"': invalid month '"+monthS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                }
    +            }
    +            
    +            if (Strings.isNonBlank(tz)) {
    +                TimeZone tzz = null;
    +                String tzCode = m.group("tzCode");
    +                if (Strings.isNonBlank(tzCode)) {
    +                    tz = tzCode;
    +                }
    +                if (tz.matches(DIGIT+"+")) {
    +                    // stick a plus in front in case it was submitted by a web form and turned into a space
    +                    tz = "+"+tz;
    +                } else {
    +                    tzz = getTimeZone(tz);
    +                }
    +                if (tzz==null) {
    +                    Maybe<Matcher> tmm = match("^ ?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")"+optionally(optionally(":")+namedGroup("tzM", DIGIT+DIGIT))+"$", tz);
    +                    if (tmm.isAbsent()) {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid timezone '"+tz+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    Matcher tm = tmm.get();
    +                    String tzM = tm.group("tzM");
    +                    int offset = (60*Integer.parseInt(tm.group("tzH")) + Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60;
    +                    tzz = new SimpleTimeZone(offset*1000, tz);
    +                }
    +                tz = getTimeZoneOffsetString(tzz, year, month, day);
    +                result = new GregorianCalendar(tzz);
    +            } else {
    +                result = new GregorianCalendar();
    +            }
    +            result.clear();
    +            
    +            result.set(Calendar.YEAR, year);
    +            result.set(Calendar.MONTH, month);
    +            result.set(Calendar.DAY_OF_MONTH, day);
    +            if (m.group("hours")!=null) {
    +                int hours = Integer.parseInt(m.group("hours"));
    +                String meridian = m.group("meridian");
    +                if (Strings.isNonBlank(meridian) && meridian.toLowerCase().startsWith("p")) {
    +                    if (hours>12) {
    +                        return Maybe.absent("Unknown date format '"+input+"': can't be "+hours+" PM; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    hours += 12;
    +                }
    +                result.set(Calendar.HOUR_OF_DAY, hours);
    +                String minsS = m.group("mins");
    +                if (Strings.isNonBlank(minsS)) {
    +                    result.set(Calendar.MINUTE, Integer.parseInt(minsS));
    +                }
    +                String secsS = m.group("secs");
    +                if (Strings.isBlank(secsS)) {
    +                    // leave at zero
    +                } else if (secsS.matches(DIGIT+DIGIT+"?")) {
    +                    result.set(Calendar.SECOND, Integer.parseInt(secsS));
    +                } else {
    +                    double s = Double.parseDouble(secsS);
    +                    if (s>=0 && s<=60) {
    +                        // in double format, with correct period
    +                    } else if (secsS.length()==5) {
    +                        // allow ssSSS with no punctuation
    +                        s = s/=1000;
    +                    } else {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid seconds '"+secsS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +                    }
    +                    result.set(Calendar.SECOND, (int)s);
    +                    result.set(Calendar.MILLISECOND, (int)((s*1000) % 1000));
    +                }
    +            }
    +            
    +            return Maybe.of(result);
    +        }
    +        return Maybe.absent("Unknown date format '"+input+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
    +    }
    +    
    +    public static TimeZone getTimeZone(String code) {
    +        if (code.indexOf('/')==-1) {
    +            if ("Z".equals(code)) return TIME_ZONE_UTC;
    +            if ("UTC".equals(code)) return TIME_ZONE_UTC;
    +            if ("GMT".equals(code)) return TIME_ZONE_UTC;
    +            
    +            // get the time zone -- most short codes aren't accepted, so accept (and prefer) certain common codes
    +            if ("EST".equals(code)) return getTimeZone("America/New_York");
    +            if ("EDT".equals(code)) return getTimeZone("America/New_York");
    +            if ("PST".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("CST".equals(code)) return getTimeZone("America/Chicago");
    +            if ("CDT".equals(code)) return getTimeZone("America/Chicago");
    +            if ("MST".equals(code)) return getTimeZone("America/Denver");
    +            if ("MDT".equals(code)) return getTimeZone("America/Denver");
    +
    +            if ("BST".equals(code)) return getTimeZone("Europe/London");  // otherwise BST is Bangladesh!
    +            if ("CEST".equals(code)) return getTimeZone("Europe/Paris");
    +            // IST falls through to below, where it is treated as India (not Irish); IDT not recognised
    +        }
    +        
    +        TimeZone tz = TimeZone.getTimeZone(code);
    +        if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) {
    +            // recognized
    +            return tz;
    +        }
    +        // possibly unrecognized -- GMT returned if not known, bad TimeZone API!
    +        String timeZones[] = TimeZone.getAvailableIDs();
    +        for (String tzs: timeZones) {
    +            if (tzs.equals(code)) return tz;
    +        }
    +        // definitely unrecognized
    +        return null;
    +    }
    +    
    +    /** convert a TimeZone e.g. Europe/London to an offset string as at the given day, e.g. +0100 or +0000 depending daylight savings,
    +     * absent with nice error if zone unknown */
    +    public static Maybe<String> getTimeZoneOffsetString(String tz, int year, int month, int day) {
    +        TimeZone tzz = getTimeZone(tz);
    +        if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz);
    +        return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day));
    +    }
    +    
    +    /** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the {@link TimeZone} is already instantiated */
    +    @SuppressWarnings("deprecation")
    +    public static String getTimeZoneOffsetString(TimeZone tz, int year, int month, int day) {
    +        int tzMins = tz.getOffset(new Date(year, month, day).getTime())/60/1000;
    +        String tzStr = (tzMins<0 ? "-" : "+") + Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", "")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", "");
    +        return tzStr;
    +    }
    +
    +    private static String namedGroup(String name, String pattern) {
    +        return "(?<"+name+">"+pattern+")";
    +    }
    +    private static String anyChar(String charSet) {
    +        return "["+charSet+"]";
    +    }
    +    private static String optionally(String pattern) {
    +        return "("+pattern+")?";
    +    }
    +    private static String options(String ...patterns) {
    +        return "("+Strings.join(patterns,"|")+")";
    +    }
    +    private static String notMatching(String pattern) {
    +        return "(?!"+pattern+")";
    +    }
    +    
    +    private static Maybe<Matcher> match(String pattern, String input) {
    +        Matcher m = Pattern.compile(pattern).matcher(input);
    +        if (m.find() && m.start()==0 && m.end()==input.length())
    --- End diff --
    
    yeah - that occurred to me later.  i'll do it.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit closed the pull request at:

    https://github.com/apache/incubator-brooklyn/pull/682


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-109918871
  
    Build failure is due to Java 6 being used on Jenkins machine.  If Java 7 minimum is agreed let's update Jenkins and kick off a new build.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by alasdairhodge <gi...@git.apache.org>.
Github user alasdairhodge commented on a diff in the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#discussion_r31898457
  
    --- Diff: utils/common/src/main/java/brooklyn/util/time/Time.java ---
    @@ -443,36 +472,433 @@ else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreC
             }
         }
     
    +    /** parses dates from string, accepting many formats including 'YYYY-MM-DD', 'YYYY-MM-DD HH:mm:SS', or millis since UTC epoch */
    +    public static Date parseDate(String input) {
    +        if (input==null) return null;
    +        return parseDateMaybe(input).get();
    +    }
    +    
    +    /** as {@link #parseDate(String)} but returning a {@link Maybe} rather than throwing or returning null */
    +    public static Maybe<Date> parseDateMaybe(String input) {
    +        if (input==null) return Maybe.absent("value is null");
    +        input = input.trim();
    +        Maybe<Date> result;
    +
    +        result = parseDateUtc(input);
    +        if (result.isPresent()) return result;
    +
    +        result = parseDateSimpleFlexibleFormatParser(input);
    +        if (result.isPresent()) return result;
    +        // return the error from this method
    +        Maybe<Date> returnResult = result;
    +
    +//        // see natty method comments below
    +//        Maybe<Date> result = parseDateNatty(input);
    +//        if (result.isPresent()) return result;
    +
    +        result = parseDateFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING));
    +        if (result.isPresent()) return result;
    +        result = parseDateDefaultParse(input);
    +        if (result.isPresent()) return result;
    +
    +        return returnResult;
    +    }
    +
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Date> parseDateDefaultParse(String input) {
    +        try {
    +            long ms = Date.parse(input);
    +            if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
    +                // accept default date parse for this century and next
    +                return Maybe.of(new Date(ms));
    +            }
    +        } catch (Exception e) {
    +            Exceptions.propagateIfFatal(e);
    +        }
    +        return Maybe.absent();
    +    }
    +
    +    private static Maybe<Date> parseDateUtc(String input) {
    +        if (input.matches("\\d+")) {
    +            Maybe<Date> result = Maybe.of(new Date(Long.parseLong(input)));
    +            if (result.isPresent()) {
    +                @SuppressWarnings("deprecation")
    +                int year = result.get().getYear();
    +                if (year >= 2000 && year < 2200) {
    +                    // only applicable for dates in this century
    +                    return result;
    +                } else {
    +                    return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
    +                }
    +            }
    +        }
    +        return Maybe.absent("not long millis since epoch UTC");
    +    }
    +
    +    private final static String DIGIT = "\\d";
    +    private final static String LETTER = "\\p{L}";
    +    private final static String COMMON_SEPARATORS = "-\\.";
    +    private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
    +    private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
    +    private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
    +
    +    private final static String DATE_ONLY_WITH_INNER_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+"))
    +            + anyChar(DATE_SEPARATOR)
    +            + namedGroup("day", optionally(DIGIT)+DIGIT);
    +    private final static String DATE_WORDS_2 = 
    +        namedGroup("month", anyChar(LETTER)+"+")
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +    // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
    +    private final static String DATE_WORDS_3 = 
    +        namedGroup("day", optionally(DIGIT)+DIGIT)
    +        + anyChar(DATE_SEPARATOR)
    +        + namedGroup("month", anyChar(LETTER)+"+")
    +        + ",?"+anyChar(DATE_SEPARATOR)+"+"
    +        + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
    +
    +    private final static String DATE_ONLY_NO_SEPARATORS = 
    +            namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT)
    +            + namedGroup("month", DIGIT+DIGIT)
    +            + namedGroup("day", DIGIT+DIGIT);
    +
    +    private final static String MERIDIAN = anyChar("aApP")+optionally(anyChar("mM"));
    +    private final static String TIME_ONLY_WITH_INNER_SEPARATORS = 
    +        namedGroup("hours", optionally(DIGIT)+DIGIT)+
    +        optionally(
    +            anyChar(TIME_SEPARATOR)+
    +            namedGroup("mins", DIGIT+DIGIT)+
    +            optionally(
    +                anyChar(TIME_SEPARATOR)+
    +                namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+"))))+
    +        optionally(" *" + namedGroup("meridian", notMatching(LETTER+LETTER+LETTER)+MERIDIAN));
    +    private final static String TIME_ONLY_NO_SEPARATORS = 
    +        namedGroup("hours", DIGIT+DIGIT)+
    +        namedGroup("mins", DIGIT+DIGIT)+
    +        optionally(
    +            namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+")))+
    +        namedGroup("meridian", "");
    +
    +    private final static String TZ_CODE = namedGroup("tzCode",
    +        notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) // not AM or PM
    +        + anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*");
    +    private final static String TIME_ZONE_SIGNED_OFFSET = namedGroup("tz", options(namedGroup("tzOffset", options("\\+", "-")+
    +            DIGIT+optionally(DIGIT)+optionally(optionally(":")+DIGIT+DIGIT)), 
    +        optionally("\\+")+TZ_CODE));
    +    private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET = namedGroup("tz", 
    +        options(
    +            namedGroup("tzOffset", options("\\+", "-", " ")+
    +                options("0"+DIGIT, "10", "11", "12")+optionally(optionally(":")+DIGIT+DIGIT)), 
    +            TZ_CODE));
    +
    +    private static String getDateTimeSeparatorPattern(String extraChars) {
    +        return options(" +"+optionally(anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")),
    +                anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+","))
    +            + anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars)+"*";
    +    }
    +    
    +    @SuppressWarnings("deprecation")
    +    private static Maybe<Date> parseDateSimpleFlexibleFormatParser(String input) {
    +        input = input.trim();
    +
    +        String[] DATE_PATTERNS = new String[] {
    +            DATE_ONLY_WITH_INNER_SEPARATORS,
    +            DATE_ONLY_NO_SEPARATORS,
    +            DATE_WORDS_2,
    +            DATE_WORDS_3,            
    +        };
    +        String[] TIME_PATTERNS = new String[] {
    +            TIME_ONLY_WITH_INNER_SEPARATORS,
    +            TIME_ONLY_NO_SEPARATORS            
    +        };
    +        String[] TZ_PATTERNS = new String[] {
    +            // space then time zone with sign (+-) or code is preferred
    +            optionally(getDateTimeSeparatorPattern("")) + " " + TIME_ZONE_SIGNED_OFFSET,
    +            // then no TZ - but declare the named groups
    +            namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", "")),
    +            // then any separator then offset with sign
    +            getDateTimeSeparatorPattern("") + TIME_ZONE_SIGNED_OFFSET,
    +            
    +            // try parsing with enforced separators before TZ first 
    +            // (so e.g. in the case of DATE-0100, the -0100 is the time, not the timezone)
    +            // then relax below (e.g. in the case of DATE-TIME+0100)
    +            
    +            // finally match DATE-TIME-1000 as time zone -1000
    +            // or DATE-TIME 1000 as TZ +1000 in case a + was supplied but converted to ' ' by web
    +            // (but be stricter about the format, two or four digits required, and hours <= 12 so as not to confuse with a year)
    +            optionally(getDateTimeSeparatorPattern("")) + TIME_ZONE_OPTIONALLY_SIGNED_OFFSET
    +        };
    +        
    +        List<String> basePatterns = MutableList.of();
    +        
    +        // patterns with date first
    +        String[] DATE_PATTERNS_UNCLOSED = new String[] {
    +            // separator before time *required* if date had separators
    +            DATE_ONLY_WITH_INNER_SEPARATORS + "("+getDateTimeSeparatorPattern("T"),
    +            // separator before time optional if date did not have separators
    +            DATE_ONLY_NO_SEPARATORS + "("+optionally(getDateTimeSeparatorPattern("T")),
    +            // separator before time required if date has words
    +            DATE_WORDS_2 + "("+getDateTimeSeparatorPattern("T"),
    +            DATE_WORDS_3 + "("+getDateTimeSeparatorPattern("T"),
    +        };
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS_UNCLOSED)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + dateP + timeP+")?" + tzP + "$");
    +        
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + getDateTimeSeparatorPattern("") + dateP + tzP + "$");
    +        // also allow time first, with TZ after, then before
    +        for (String tzP: TZ_PATTERNS)
    +            for (String dateP: DATE_PATTERNS)
    +                for (String timeP: TIME_PATTERNS)
    +                    basePatterns.add("^" + timeP + tzP + getDateTimeSeparatorPattern("") + dateP + "$");
    +
    +        Maybe<Matcher> mm = Maybe.absent();
    +        for (String p: basePatterns) {
    +            mm = match(p, input);
    +            if (mm.isPresent()) break;
    +        }
    +        if (mm.isPresent()) {
    +            Matcher m = mm.get();
    +            Calendar result;
    +
    +            String tz = m.group("tz");
    +            
    +            int year = Integer.parseInt(m.group("year"));
    +            int day = Integer.parseInt(m.group("day"));
    +            
    +            String monthS = m.group("month");
    +            int month;
    +            if (monthS.matches(DIGIT+"+")) {
    +                month = Integer.parseInt(monthS)-1;
    +            } else {
    +                try {
    +                    month = new SimpleDateFormat("yyyy-MMM-dd").parse("2015-"+monthS+"-15").getMonth();
    +                } catch (ParseException e) {
    +                    return Maybe.absent("Unknown date format '"+input+"': invalid month '"+monthS+"'; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
    +                }
    +            }
    +            
    +            if (Strings.isNonBlank(tz)) {
    +                TimeZone tzz = null;
    +                String tzCode = m.group("tzCode");
    +                if (Strings.isNonBlank(tzCode)) {
    +                    tz = tzCode;
    +                }
    +                if (tz.matches(DIGIT+"+")) {
    +                    // stick a plus in front in case it was submitted by a web form and turned into a space
    +                    tz = "+"+tz;
    +                } else {
    +                    tzz = getTimeZone(tz);
    +                }
    +                if (tzz==null) {
    +                    Maybe<Matcher> tmm = match("^ ?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")"+optionally(optionally(":")+namedGroup("tzM", DIGIT+DIGIT))+"$", tz);
    +                    if (tmm.isAbsent()) {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid timezone '"+tz+"'; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
    +                    }
    +                    Matcher tm = tmm.get();
    +                    String tzM = tm.group("tzM");
    +                    int offset = (60*Integer.parseInt(tm.group("tzH")) + Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60;
    +                    tzz = new SimpleTimeZone(offset*1000, tz);
    +                }
    +                tz = getTimeZoneOffsetString(tzz, year, month, day);
    +                result = new GregorianCalendar(tzz);
    +            } else {
    +                result = new GregorianCalendar();
    +            }
    +            result.clear();
    +            
    +            result.set(Calendar.YEAR, year);
    +            result.set(Calendar.MONTH, month);
    +            result.set(Calendar.DAY_OF_MONTH, day);
    +            if (m.group("hours")!=null) {
    +                int hours = Integer.parseInt(m.group("hours"));
    +                String meridian = m.group("meridian");
    +                if (Strings.isNonBlank(meridian) && meridian.toLowerCase().startsWith("p")) {
    +                    if (hours>12) {
    +                        return Maybe.absent("Unknown date format '"+input+"': can't be "+hours+" PM; try 'yyyy-MM-dd HH:mm:ss.SSS +0000'");
    +                    }
    +                    hours += 12;
    +                }
    +                result.set(Calendar.HOUR_OF_DAY, hours);
    +                String minsS = m.group("mins");
    +                if (Strings.isNonBlank(minsS)) {
    +                    result.set(Calendar.MINUTE, Integer.parseInt(minsS));
    +                }
    +                String secsS = m.group("secs");
    +                if (Strings.isBlank(secsS)) {
    +                    // leave at zero
    +                } else if (secsS.matches(DIGIT+DIGIT+"?")) {
    +                    result.set(Calendar.SECOND, Integer.parseInt(secsS));
    +                } else {
    +                    double s = Double.parseDouble(secsS);
    +                    if (s>=0 && s<=60) {
    +                        // in double format, with correct period
    +                    } else if (secsS.length()==5) {
    +                        // allow ssSSS with no punctuation
    +                        s = s/=1000;
    +                    } else {
    +                        return Maybe.absent("Unknown date format '"+input+"': invalid seconds '"+secsS+"'; try 'YYYY-MM-DD HH:mm:ss.SSS +0000'");
    +                    }
    +                    result.set(Calendar.SECOND, (int)s);
    +                    result.set(Calendar.MILLISECOND, (int)((s*1000) % 1000));
    +                }
    +            }
    +            
    +            return Maybe.of(result.getTime());
    +        }
    +        return Maybe.absent("Unknown date format '"+input+"'; try ISO-8601, or 'yyyy-MM-dd' or 'yyyy-MM-dd HH:mm:ss +0000'");
    +    }
    +    
    +    public static TimeZone getTimeZone(String code) {
    +        if (code.indexOf('/')==-1) {
    +            if ("Z".equals(code)) return getTimeZone("UTC");
    +            
    +            // get the time zone -- most short codes aren't accepted, so accept (and prefer) certain common codes
    +            if ("EST".equals(code)) return getTimeZone("America/New_York");
    +            if ("EDT".equals(code)) return getTimeZone("America/New_York");
    +            if ("PST".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles");
    +            if ("CST".equals(code)) return getTimeZone("America/Chicago");
    +            if ("CDT".equals(code)) return getTimeZone("America/Chicago");
    +            if ("MST".equals(code)) return getTimeZone("America/Denver");
    +            if ("MDT".equals(code)) return getTimeZone("America/Denver");
    +
    +            if ("BST".equals(code)) return getTimeZone("Europe/London");  // otherwise BST is Bangladesh!
    +            if ("CEST".equals(code)) return getTimeZone("Europe/Paris");
    +            // IST falls through to below, where it is treated as India (not Irish); IDT not recognised
    +        }
    +        
    +        TimeZone tz = TimeZone.getTimeZone(code);
    +        if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) {
    +            // recognized
    +            return tz;
    +        }
    +        // possibly unrecognized -- GMT returned if not known, bad TimeZone API!
    +        String timeZones[] = TimeZone.getAvailableIDs();
    +        for (String tzs: timeZones) {
    +            if (tzs.equals(code)) return tz;
    +        }
    +        // definitely unrecognized
    +        return null;
    +    }
    +    
    +    /** convert a TimeZone e.g. Europe/London to an offset string as at the given day, e.g. +0100 or +0000 depending daylight savings,
    +     * absent with nice error if zone unknown */
    +    public static Maybe<String> getTimeZoneOffsetString(String tz, int year, int month, int day) {
    +        TimeZone tzz = getTimeZone(tz);
    +        if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz);
    +        return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day));
    +    }
    +    
    +    /** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the {@link TimeZone} is already instantiated */
    +    @SuppressWarnings("deprecation")
    +    public static String getTimeZoneOffsetString(TimeZone tz, int year, int month, int day) {
    +        int tzMins = tz.getOffset(new Date(year, month, day).getTime())/60/1000;
    +        String tzStr = (tzMins<0 ? "-" : "+") + Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", "")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", "");
    +        return tzStr;
    +    }
    +
    +    private static String namedGroup(String name, String pattern) {
    +        return "(?<"+name+">"+pattern+")";
    +    }
    +    private static String anyChar(String charSet) {
    +        return "["+charSet+"]";
    +    }
    +    private static String optionally(String pattern) {
    +        return "("+pattern+")?";
    +    }
    +    private static String options(String ...patterns) {
    +        return "("+Strings.join(patterns,"|")+")";
    --- End diff --
    
    Would a non-capturing group be better?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] incubator-brooklyn pull request: Date parsing and type coercion fi...

Posted by ahgittin <gi...@git.apache.org>.
Github user ahgittin commented on the pull request:

    https://github.com/apache/incubator-brooklyn/pull/682#issuecomment-110854822
  
    @sjcorbett I think the more general argument you are making is for a feature to have a "strict" mode where a much more restrictive set of type coercions is applied (failing or logging otherwise).  We'd require ISO 8601 here.
    
    I can see this being useful when we get to the point of interoperability and validation.  But for where we are now I think the priority is to remove any barriers towards understanding and writing blueprints.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---