Welcome, guest | Sign In | My Account | Store | Cart

... a light meal with a heavy dose of "tutorial mash" on the side.

In the constructive spirit of "more ways to solve a problem"; this is a portion of my lateral, occasionally oblique, solutions. Nothing new in le régime de grande, but hopefully the conceptual essence will amuse.

Initially started as a response to recipe 577135 which parses incremental date fragments and preserves micro-seconds where available. That script does more work than this, for sure, but requires special flow-control and iterates a potentially incumbering shopping list (multi-dimensional with some detail).

So here's a different box for others to play with. Upside-down in a sense, it doesn't hunt for anything but a numerical "pulse"; sequences of digits punctuated by other 'stuff' we don't much care about.

Missing a lot of things, intentionally, this snippet provides several examples demoin' flexibility. Easy to button-up, redecorate and extend later for show, till then the delightful commentary makes it hard enough to see bones already -- all six lines or so!

Note: The core script is repeated for illustrative purposes. The first is step-by-step, the second is lean and condensed for utilitarian purposes. It is the second, shorter, version that I yanked from a file and gussied up.

Python, 139 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env python
"""
2012-03-05, weeee!

This is a really simple script, the docs are WAY longer, that
dices a date-string returning a list of integers or a dict if
key-words are supplied.

... IT SLICES, IT DICES, IT HAS SHARP EDGES!
    ===============================================
    Not production-ready, 'nless you like to play with razors.
    There is no type-checking, no assertion for field-order etc.
    This simply, blindly and unintelligently guts the string.   
    If the order changes, it bites... you get the idea.

Some examples, more plus arg-defs b'low

    TEST_DATE = "2012-03-05 13:05:14.453728"
    
    # return list of int's in original order
    cheap_date(TEST_DATE)
    [2012, 3, 5, 13, 5, 14, 453728]
    
    ISO_KEYS = ['t_year','t_mon','...'t_sec','t_usec']

    # return same list, mapped into a dict
    cheap_date(TEST_DATE, ISO_KEYS)
    {'t_mon': 3, 't_min': 5, 't_sec': 14, 't_hour': 13,
              't_day': 5, 't_year': 2012, 't_usec': 453728}
    
    # Keep the decimal t'gether using non-default regex
    # Note: list is str's, int("12.34") razors a ValueError
    cheap_date(TEST_DATE, [], DIG_N_DEC, str)
    ['2012', '03', '05', '13', '05', '14.453728']
    
    # dict's and format strings, naturally sweeeeet
    FMT_STR % cheap_date(TEST_DATE, ISO_KEYS, DIG_N_DEC, PAT)
    2012-03-05T13:05:14.454
    
    FMT_STR2 % cheap_date(TEST_DATE, ISO_KEYS, val_conv = str)
    13:05-03/05/2012

"""

import re

def cheap_date(dt_str, kw_list = [], reg_xp = r'\D', val_conv = int):
    """ Cheap incremental date parser preserving ISO microseconds

    dt_str:     String representing source date
           pass "2012-03-05 13:05:14.453728"
        returns [2012, 3, 5, 13, 5, 14, 453728]

      optional- ['2012', '03', '05', '13', '05', '14.453728']
        returns {'tm_year': 2012, 'tm_mday': 5, 'tm_mon': 3 ... }

    Optional arguments:

    kw_list:    Ordered list of return-dictionary keys
    reg_xp:     Regular expression used to split the string
    val_conv:   List-processor for data-conversion i.e. str --> int

    >>> cheap_date(TEST_DATE)
    [2012, 3, 5, 13, 5, 14, 453728]
    >>> cheap_date(TEST_DATE, ISO_KEYS[:3])
    {'t_mon': 3, 't_day': 5, 't_year': 2012}
    >>> FMT_STR2 % cheap_date(TEST_DATE, ISO_KEYS, val_conv = str)
    '13:05-03/05/2012'
    """
    # shake the numbers out with re ['2012', '03'...]
    tm_list = re.split(reg_xp, dt_str)
    
    # juice 'em: apply function to each list-value [2012, 03...]
    # you _could_ test if val_conv == str an omit this step
    tm_list = map(val_conv, tm_list)

    # Existence of this list, enables return of a dictionary
    if kw_list:
        # fabricate list of key-value pairs [['yr',2012],[....],]
        tm_list = zip(kw_list, tm_list)
        
        # map the key-val pairs into a dict to be proud of
        tm_list = dict(tm_list)
    
    return tm_list


def cheaper_date(dt_str, kw_list = [], reg_xp = r'\D', val_conv = int):
    """ Cheaper date parser, with a few less teeth

    >>> FMT_STR2 % cheaper_date(TEST_DATE, ISO_KEYS, val_conv = str)
    '13:05-03/05/2012'
    """
    # The functionality above, tucked in a thin blankie.
    try:
        tm_list = map(val_conv, re.split(reg_xp, dt_str))
    except ValueError, e:
        print "Conversion proc (int?) spewed a matched value"
        print e
        raise
        
    if kw_list:
        tm_list = dict(zip(kw_list, tm_list))

    return tm_list

if __name__ == '__main__':
    import doctest

    # Some Q&D convenience, ta get 'r done.
    TEST_DATE = "2012-03-05 13:05:14.453728"

    # Keys match  number-seq. order of date to parse
    ISO_KEYS = ['t_year','t_mon','t_day','t_hour','t_min','t_sec','t_usec']

    # Slow lrner moi! Ages till I grep'd the non-obv. & betwix da lines.
    # A.K.A: "To select or not select? TITQ!" I mean "\d" to "\D" 

    # The following exp. splits, and discards, NON-number sequences.
    DIGITS_ONLY = r'\D' # DEFAULT, digits only: 12.56-> ['12','56']

    # \d is inverse|not \D, [^....] inverse|not's the match 
    DIG_N_DEC = r'[^\d\.]' # retain decmal no's 34.78-> ['34.78',]
    
    # With a dict[ionary] and format strings, it happens eh?
    FMT_STR = "T".join(["%(t_year)d-%(t_mon)02d-%(t_day)02d",
                                "%(t_hour)02d:%(t_min)02d:%(t_sec)0.3f"])
    # Same info, just shuffled for my simple-minded amusement
    FMT_STR2 = "%(t_hour)s:%(t_min)s-%(t_mon)s/%(t_day)s/%(t_year)s"
    
    # Pick-A-Type... for demo. Its stupid, assumes a string of digits only.
    # There are safer/elegantisher ways to do this... more calories though.
    def PAT(s): 
        try:
            return int(s)
        except ValueError:
            return float(s)

    doctest.testmod()

Sadly, the working-bits are only a few lines, it took obscenely longer to carve it from the lib., document then add my 'helpful' commentary. But why stop there, there's room here too!

Joking aside, the following is intended for beginners trying to understand what's going on here, and a bit of elsewhere too.

Date and time strings are encoded-sequences of characters with implied labels. As long as we infer the positional labels properly, all is golden.

The prime example for this discussion is this puppy:

2012-03-01T13:00:00

Large, coarse values to small, progressing from most significant on the left to the least on the right. This clear date-structure is predictable and adaptable.

It is this way for good reason as ambiguity is expensive, but also it is part of the ISO 8601 spec. It can maintain integrity even while loosing accuracy. Meaning '2012-03' is still valid, interpretable and meets the standard.

A common approach is to go hunting with pre-defined patterns, seeking a match within the target data. The following pattern precisely interprets an isolated sequence for specific elements of date and time.

"%Y-%m-%d %H:%M:%S"

Stalking "conventional" patterns within character sequences is effective as long as the data is consistent and clean. Python can fish wee date sequences from oceans of non-sequences. Using date & time libraries is common and generally practical, however the space between the raw data and libraries that gets interesting.

But we don't need to hunt, sometimes a simple shake will relieve branches of their prize(s). With ISO format, and others, we know exactly which patterns are where within the sequence.

The relevant date-portion of the sequence is 4-1-2-1-2 when viewing the digit to delimiter relationships. With a teeny bit of work, python's string manipulation makes short work of this predictable format. All fine till an exception rolls along.

The old newspaper joke "Editorial is useful to space the advertising" is essentially true here too. It's all about the digits, the other stuff is just filler.

It's the exceptions that trip us, alterations that frustrate us and unexpected change that often 'toasts' us.

Recipe 577135 solves the ISO incremental representation problems. It can extract month-resolution as easily as "by the second" date-time sequences. It employs a table of increasing sequences to extract what it can.

formats = ["YYYY", "YYYY-MM", "YYYY-MM-DD", ...]

The concept is to begin from either end, apply to the target until reacing the success VS failure threshold. The largest successful match is usually the keeper. From one small script, extract '2012-03' and '2012-03-05T12:02', etc., nice.

For the "coding-rodeo's" I've been in date-strings haven't been so pretty, especially those with a mix of important text and digits. In fact, they often look about as good as real rodeo arena's and pens or stables after a weekend stampede!

For the above recipe, the 're' library and it's 'split' method made shredding date-strings trivial. Following up with its documentation, you'll discover it is a steroid-driven, monster-version of python's string.split()

Without delving deeply into regular expressions, the difference is how divisions are specified. In python a single matching specification can be applied to a string. Subsequent splits require specification and iteration across prevous segments.

With 're' and 'grep' pattern specifications, a vast array of factors can be considered before a line is cleaved. In the case of this script, though, it isn't so high-brow.

The default pattern matches anything that is not a digit, divides the string at that point, then continues onto the next. This means the hyphens, spaces and colons are all recognized delimiters enabling the following.

re.split("\D", "2012-03-05")
        returns --> ['2012', '03', '05']

Using python's slice syntax, the string is blindly carvable. Fragile if any of the positions shift due to extra characters, etc.

t_year = date_var[:4]
t_mon  = date_var[5:7]
t_day  = date_var[-2:]

As well, the following non-ISO are easily parsable based upon this simple approach. Modifications are needd for some things, but overall its adaptable. Like above, this is accomplished with only one 'split' instruction, python slicing, etc., would require more.

2/6/91          --> ['2', '6', '91']
19880601-120231 --> ['19880601', '120231']
23.01.09        --> ['23', '01', '09']

Note: To the specific matching pattern "it all tastes like chicken" when it comes to punctuation, and any other non-digit characters.

Given some insight into significance of sequential order, the above is easily mapped into a dictionary, other formats or into a value for computational purposes.

Observant may have noticed the results are lists of strings. '01' is not equal to 1 but int('01') is. The map function used, by default, will visit each element of a list and attempt to replace the strings with integers. There are circumstances, if the expression is altered, that ValueErrors can occur.

map(int, ['2012', '03', '05']) --> [2012, 03, 05]

Note: This 3-element integer list can be extended for use with date & time libraries:

date_list = map(int, ['2012', '03', '05']) # convert to integers
    --> [2012, 03, 05]
date_list.extend([0,0,0,0,0,0])
    --> [2012, 03, 05, 0, 0, 0, 0, 0, 0]
time.mktime([2012, 03, 05, 0, 0, 0, 0, 0, 0])
    --> 1330930800.0

The next, pertinent, consideration is the significance of sequential order. In the main example its obviously year, month, day, etc. However with others it isn't so obious. The first non ISO example above is ambiguous, the second is clear but requres some work and the third is clear.

Pyhton's zip function, not to be confused with the compression application, marries two separate lists into fresh lists of key-value pairs. Converting the resultant ordered pairs into a dict keys the values.

zip([1,2,3],['a','b','c']) --> [[1:'a'],[2:'b'],[3:'c']]
dict([[1:'a'],[2:'b'],[3:'c']]) --> {1:'a',2:'b',3:'c'}


2/6/91:  dict(zip(['mth','day','year'],['2','6','91'])
        --> {'mth': '6','day': '2','year': '91'}
23.01.09:  dict(zip(['day','mth','year'],['23', '01', '09'])
        --> {'day': '23','mth': '01','year': 09'}

Bonus points!

Mis-matched length operations can cause range, key and value errors when being manipulated outside their bounds. The zip function, however, gracefully stops iterating when one or both lists are exhausted.

This cleanly accommodates the variable sized ISO dates allowing the year-month-day dates to function as easily as those including the microseconds portion. This also means, if you only require the first three fields in a dictionary, passing only those three keys automatically truncates the remaining values. This will not work out of sequence, only left-to-right at this time.

Happy trails!