Lo siento, esto puede parecer realmente específico, pero se reduce a un problema de sed que no puedo resolver.

Contexto: Estoy tratando de escribir una función bash que toma una duración de formato ISO 8601 arbitraria (por ejemplo, P1DPT12M1S, P15MPT12M40S, etc.) y la convierte en una cadena que puedo usar para agregar a una fecha a través de algo como: date -d "$(date) + 1 day + 1 minute"

Esto es lo que tengo hasta ahora:

duration_parser() {\
    duration=$1\
    duration=$(sed 's/PT\(.*\)\([[:digit:]]\)S/PT\1 + \2 second/g' <<< $duration)
    duration=$(sed 's/PT\(.*\)\([[:digit:]]\)M/PT\1 + \2 minute/g' <<< $duration)
    duration=$(sed 's/PT\(.*\)\([[:digit:]]\)H/PT\1 + \2 hour/g' <<< $duration)
    duration=$(sed 's/PT//g' <<< $duration)
    duration=$(sed 's/P\(.*\)\([[:digit:]]\)D/P\1 + \2 day/g' <<< $duration)
    duration=$(sed 's/P\(.*\)\([[:digit:]]\)W/P\1 + \2 week/g' <<< $duration)
    duration=$(sed 's/P\(.*\)\([[:digit:]]\)M/P\1 + \2 month/g' <<< $duration)
    duration=$(sed 's/P\(.*\)\([[:digit:]]\)Y/P\1 + \2 year/g' <<< $duration)
    duration=$(sed 's/P//g' <<< $duration)
    echo $duration
}\

date -d "$(date) $(duration_parser PT6M3S)"

Lo que funciona para duraciones en las que cada unidad es un dígito, p. Ej.

date -d "$(date) $(duration_parser PT6M3S)"

Funciona, pero cuando las unidades son de varios dígitos, como 60 minutos:

date -d "$(date) $(duration_parser PT60M3S)"

No es asi. Parece que no puedo conseguir sed para recoger todos los dígitos ...

¿Hay alguna manera de hacer esto con sed, o es posible que esta no sea la mejor manera de hacerlo y hay una manera más fácil? ja ja

0
jchen0022 2 oct. 2019 a las 23:51

1 respuesta

La mejor respuesta

sed tiene su propio lenguaje, donde puede encadenar comandos, incluso manejar errores.

Terminé con esto:

dur_to_dateadd() {
    # https://en.wikipedia.org/wiki/ISO_8601#Durations
    # PnYnMnDTnHnMnS <- we handle only this
    <<<"$1" sed -E '
        # it has to start with p
        /^P/!{
            s/.*/ERROR: Invalid input - it has to start with P: "&"/
            q1
        }
        s/^P//

        # add an unredable 0x01 on the end
        # it serves as our "line separator"
        s/$/\x01/

        # parse from the beginning, add to the end after \x01
        s/^([0-9]*([,.][0-9]*)?)Y(.*)/\3 + \1 year/
        s/^([0-9]*([,.][0-9]*)?)M(.*)/\3 + \1 month/
        s/^([0-9]*([,.][0-9]*)?)D(.*)/\3 + \1 day/
        /^T/{
            s///
            s/^([0-9]*([,.][0-9]*)?)H(.*)/\3 + \1 hour/
            s/^([0-9]*([,.][0-9]*)?)M(.*)/\3 + \1 minute/
            s/^([0-9]*([,.][0-9]*)?)S(.*)/\3 + \1 second/
        }

        # we should have parsed it all
        # so our separator \x01 has to be the first character
        /^\x01/!{
          # there is something unparsed in the input
            s/\x01.*//
            s/.*/ERROR: Unparsable input: "&"/
            q1
        }
        # remove the \x01
        s///

        # just convert , to . in case of floats
        s/,/./g
    '
}

dur_to_dateadd "P3Y6M4DT12H30M5S"
dur_to_dateadd "P23DT23H"
dur_to_dateadd "P4Y"
dur_to_dateadd "PT0S"
dur_to_dateadd "P0D"
dur_to_dateadd "P1M"
dur_to_dateadd "PT1M"
dur_to_dateadd "P0,5Y"
dur_to_dateadd "P0.5Y"
dur_to_dateadd "PT36H"
dur_to_dateadd "P1DT12H"
dur_to_dateadd "invalid" || echo error
dur_to_dateadd "P1Dinvalid" || echo error
dur_to_dateadd "PinvalidDT" || echo error

Que produce:

 + 3 year + 6 month + 4 day + 12 hour + 30 minute + 5 second
 + 23 day + 23 hour
 + 4 year
 + 0 second
 + 0 day
 + 1 month
 + 1 minute
 + 0.5 year
 + 0.5 year
 + 36 hour
 + 1 day + 12 hour
ERROR: Invalid input - it has to start with P: "invalid"
error
ERROR: Unparsable input: "invalid"
error
ERROR: Unparsable input: "invalidDT"
error

Probado en repl.

Breve descripción: Primero elimino el ^P inicial y agrego un carácter ilegible \x01 al final de la entrada. Sirve como un "separador de línea" que separa la entrada sin analizar de la cadena de entrada / salida analizada. Luego analizamos la entrada desde el principio - manejamos ^<number>Y, luego ^<number>M y así sucesivamente. Si, por ejemplo, ^<number>Y coincide, agregamos + \1 year al final de la cadena después de \x01 y cualquier otra cosa. Las partes analizadas se eliminan a medida que las analizamos. Luego viene una pequeña comprobación de errores: si se analiza todo lo de la entrada, \x01 debería ser el primer carácter en el espacio del patrón. Si es así, lo quitamos y terminamos: imprimimos el espacio del patrón.

Solo por diversión, a continuación también agregué soporte para manejar formatos PnW PYYYYMMDDThhmmss y PYYYY-MM-DDThh:mm:ss, son fáciles de analizar, puede hacer coincidir todo con una sola coincidencia.

dur_to_dateadd() {
    # https://en.wikipedia.org/wiki/ISO_8601#Durations
    # We support formats:
    # PnYnMnDTnHnMnS
    # PnW
    # PYYYYMMDDThhmmss 
    # PYYYY-MM-DDThh:mm:ss
    <<<"$1" sed -E '
        # it has to start with p
        /^P/!{
            s/.*/ERROR: Invalid input - it has to start with P: "&"/
            q1
        }
        s///

        # add an unredable 0x01 on the end
        # it serves as our "line separator"
        s/$/\x01/

        # handle PnW format
        /^([0-9]*([,.][0-9]*)?)W(.*)/{
            s//\3 + \1 week/
            b finish
        }

        # handle PYYYYMMDDThhmmss format
        /^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})(.*)/{
            s//\7 + \1 year + \2 month + \3 day + \4 hour + \5 minute + \6 second/
            b finish
        }

        # handle PYYYY-MM-DDThh:mm:ss format
        /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(.*)/{
            s//\7 + \1 year + \2 month + \3 day + \4 hour + \5 minute + \6 second/
            b finish
        }


        # PnYnMnDTnHnMnS format
        # parse from the beginning, add to the end after \x01
        s/^([0-9]*([,.][0-9]*)?)Y(.*)/\3 + \1 year/
        s/^([0-9]*([,.][0-9]*)?)M(.*)/\3 + \1 month/
        s/^([0-9]*([,.][0-9]*)?)D(.*)/\3 + \1 day/
        /^T/{
            s///
            s/^([0-9]*([,.][0-9]*)?)H(.*)/\3 + \1 hour/
            s/^([0-9]*([,.][0-9]*)?)M(.*)/\3 + \1 minute/
            s/^([0-9]*([,.][0-9]*)?)S(.*)/\3 + \1 second/
        }

        : finish

        # we should have parsed it all
        # so our separator \x01 has to be the first cahracter
        /^\x01/!{
          # there is something unparsed in the input
            s/\x01.*//
            s/.*/ERROR: Unparsable input: "&"/
            q1
        }
        # remove the \x01
        s///

        # just convert , to . in case of floats
        s/,/./g
    '
}
3
KamilCuk 2 oct. 2019 a las 22:10