Translate datetime format from one standard to another In my python code, date formats are stored as "strftime and strptime syntax". For instance %d/%m/%Y %H:%M.

On the client side, I use pickadate.js which format is rather dd/mm/yyyy HH:i.

So I have to translate %d/%m/%Y %H:%M to dd/mm/yyyy HH:i before sending a datetime format to the client.

I first thought str.translate was the right tool for this. But actually, this is only for characters translation.

Then I thought about str.replace but it looks awful and inefficient to loop over str.replace:

def format_to_pickadate_format(fmt):
    trans_table = [
        ('%d', 'dd'),
        ('%a', 'ddd'),
        ('%A', 'dddd'),
        ('%m', 'mm'),
        ('%b', 'mmm'),
        ('%B', 'mmmm'),
        ('%y', 'yy'),
        ('%Y', 'yyyy'),
        ('%H', 'HH'),
        ('%M', 'i'),
        ('%p', 'A'),
    ]
    for trans in trans_table:
        fmt = fmt.replace(*trans)
    return fmt


So, to make it more elegant I used functools.reduce:

from functools import reduce

def format_to_pickadate_format(fmt):
    trans_table = [
        ('%d', 'dd'),
        ('%a', 'ddd'),
        ('%A', 'dddd'),
        ('%m', 'mm'),
        ('%b', 'mmm'),
        ('%B', 'mmmm'),
        ('%y', 'yy'),
        ('%Y', 'yyyy'),
        ('%H', 'HH'),
        ('%M', 'i'),
        ('%p', 'A'),
    ]
    return reduce(lambda x, y: x.replace(*y), trans_table, fmt)


But I think it still screams inefficiency. The string is browsed once for each sequence to replace if found.

Is there a way make it more efficient (ie. make the translation in only one iteration)?


  Note: This question is mainly for the sake of curiosity and learning. This is probably one of the least important thing I have to care about on my project, especially concerning performance.
  
  Obviously in this case, making it more efficient would be ridiculously imperceptible. If this helps you find an interest in this question, imagine another use case where I'd have a string of the size of an entire book, and a translation table of 10,000 or 100,000 entries. Doesn't it look dumb to read the book 10,000 or 100,000 times to make the translation?
The strftime function is relatively well known, even if the formatting directives are not completely standardized across platforms.  On the other hand, pickadate.js is a relatively unknown library.  It would therefore be helpful to cite its specification, preferably in a docstring.  The use of doctests would also be a good idea here.

According to the pickadate.js documentation, you should escape any “rule” characters with an exclamation mark (!).

Doing string substitutions in multiple passes is, in general, a bad idea.  Not only is it more work to traverse the string several times, there is also the possibility that operating on the output leads to incorrect results.  Fortunately, you seem to be safe here, since the pickadate.js directives don't start with % like the strftime directives.  Still, a one-pass solution is preferable, and Python gives you a way to do that with re.sub():


  re.sub(pattern, repl, string, count=0, flags=0)
  
  
    …
    
    If repl is a function, it is called for every non-overlapping occurrence of pattern. The function takes a single match object argument, and returns the replacement string.
  


Using re.sub(), and handling special cases where strings that appear to be strftime and pickadate.js directives should actually be treated literally…

import re

def format_to_pickadate_format(fmt):
    """
    Convert a strftime format string to a pickadate.js template.
    http://amsul.ca/pickadate.js/date/#formats
    http://amsul.ca/pickadate.js/time/#formats

    &gt;&gt;&gt; format_to_pickadate_format('%d/%m/%Y %H:%M')
    'dd/mm/yyyy HH:i'
    &gt;&gt;&gt; format_to_pickadate_format('%Y %% d')
    'yyyy % !d'
    """
    TRANS = {
        '%d': 'dd',
        '%a': 'ddd',
        '%A': 'dddd',
        '%m': 'mm',
        '%b': 'mmm',
        '%B': 'mmmm',
        '%y': 'yy',
        '%Y': 'yyyy',
        '%H': 'HH',
        '%M': 'i',
        '%p': 'A',
    }
    return re.sub(
        r'%(.)|([^dmyhHiaA])|(.)',
        lambda m: TRANS.get(m.group(), m.group(1)) or m.group(2) or '!' + m.group(3),
        fmt
    )