2015-07-05 11 views
5

Devo analizzare una lista di stringhe semplici con una struttura nota ma la trovo inutilmente goffo. Sento che mi manca un trucco, forse una semplice regex che renderebbe questo banale?Python - Parse string, struttura nota

La stringa si riferisce a un certo numero di anni/mesi nel futuro, voglio rendere questo in anni decimali.

formato generico: "aYbM"

dove A è il numero di anni, b è il numero di mesi questi possono essere interi ed entrambi sono opzionali (insieme con il loro identificatore)

casi di test:

5Y3M == 5.25 
5Y == 5.0 
6M == 0.5 
10Y11M = 10.91666.. 
3Y14M = raise ValueError("string '%s' cannot be parsed" %input_string) 

i miei tentativi finora hanno coinvolto stringa scissione e stato abbastanza ingombrante anche se producono i risultati corretti:

def parse_aYbM(maturity_code): 
    maturity = 0 
    if "Y" in maturity_code: 
     maturity += float(maturity_code.split("Y")[0]) 
     if "M" in maturity_code: 
      maturity += float(maturity_code.split("Y")[1].split("M")[0])/12 
     return maturity 
    elif "M" in maturity_code: 
     return float(maturity_code[:-1])/12 
    else: 
     return 0 

risposta

5

Si potrebbe usare l'espressione regolare

(?:(\d+)Y)?(?:(\d+)M)? 

che significa

(?:  start a non-grouping pattern 
    (\d+) match 1-or-more digits, grouped 
    Y  followed by a literal Y 
)?   end the non-grouping pattern; matched 0-or-1 times 
(?:  start another non-grouping pattern 
    (\d+) match 1-or-more digits, grouped 
    M  followed by a literal M 
)?   end the non-grouping pattern; matched 0-or-1 times 

Quando utilizzato in

re.match(r'(?:(\d+)Y)?(?:(\d+)M)?', text).groups() 

il metodo groups() restituisce la porzione di incontri all'interno delle parentesi di raggruppamento. None viene restituito se il gruppo non è stato abbinato. Ad esempio,

In [220]: re.match(r'(?:(\d+)Y)?(?:(\d+)M)?', '5Y3M').groups() 
Out[220]: ('5', '3') 

In [221]: re.match(r'(?:(\d+)Y)?(?:(\d+)M)?', '3M').groups() 
Out[221]: (None, '3') 

import re 
def parse_aYbM(text): 
    a, b = re.match(r'(?:(\d+)Y)?(?:(\d+)M)?', text).groups() 
    if a == b == None: 
     raise ValueError('input does not match aYbM') 
    a, b = [int(item) if item is not None else 0 for item in (a, b)] 
    return a + b/12.0 

tests = [ 
('5Y3M', 5.25), 
('5Y', 5.0), 
('6M', 0.5), 
('10Y11M', 10.917), 
('3Y14M', 4.167), 
] 

for test, expected in tests: 
    result = parse_aYbM(test) 
    status = 'Failed' 
    if abs(result - expected) < 0.001: 
     status = 'Passed' 
    print('{}: {} --> {}'.format(status, test, result)) 

cede

Passed: 5Y3M --> 5.25 
Passed: 5Y --> 5.0 
Passed: 6M --> 0.5 
Passed: 10Y11M --> 10.9166666667 
Passed: 3Y14M --> 4.16666666667 

Nota, non è chiaro quello che dovrebbe accadere se l'ingresso di parse_aYbM non corrisponde al modello. Con il codice sopra un non-partita solleva ValueError:

In [227]: parse_aYbM('foo') 
ValueError: input does not match aYbM 

ma una corrispondenza parziale può restituire un valore:

In [229]: parse_aYbM('0Yfoo') 
Out[229]: 0.0 
+0

A rigor di termini, il vostro "non-partita" è in realtà corrisponde alla stringa vuota, dal momento che entrambi i pezzi sono opzionali. Questo restituisce 'groups()' come '(None, None)'. È il tuo codice che sta sollevando l'errore ValueError, non il modulo re. Bella soluzione, però. – PaulMcG

+0

È possibile proteggersi da un numero di mesi> = 12 (come indicato nella domanda originale) con 'r" (?: (\ D +) Y)? (? :(0? \ D | 1 [01]) M) ? \ b "' - l'OP non era chiaro sulla presenza o meno di zeri iniziali. E il trailing '\ b' protegge contro la corrispondenza con un anno principale con mese non valido. – PaulMcG

+0

Grazie per la risposta dettagliata, scoppiando quello che fa l'espressione regolare! Ho trovato documentazione su regex presuppone un certo livello di conoscenza ed è quasi impossibile da leggere se non ci sei, quindi questo è davvero utile. – David258

0

È possibile utilizzare re.findall

>>> def parse(m): 
    s = 0 
    j = re.findall(r'\d+Y|\d+M', m) 
    for i in j: 
     if 'Y' in i: 
      s += float(i[:-1]) 
     if 'M' in i: 
      s += float(i[:-1])/12 
    print(s) 


>>> parse('5Y') 
5.0 
>>> parse('6M') 
0.5 
>>> parse('10Y11M') 
10.916666666666666 
>>> parse('3Y14M') 
4.166666666666667 
0

non hanno familiarità con python regex, ma provare qualcosa di simile (?<year>[^Y])\D(?<month>[^M]*)\D potrebbe solo fare il trucco.