Pergunta

Como todos sabemos, os números podem ser escritos em números ou chamados por seus nomes.Embora existam muitos exemplos que convertem 123 em cento e vinte e três, não consegui encontrar bons exemplos de como convertê-lo ao contrário.

Algumas das advertências:

  1. cardinal/nominal ou ordinal:"um" e "primeiro"
  2. erros ortográficos comuns:"quarenta"/"quarenta"
  3. centenas/milhares:2100 -> “vinte e cem” e também “dois mil e cem”
  4. separadores:"mil e cinquenta e dois", mas também "mil e cento e cinquenta e dois" ou "mil e cento e cinquenta e dois" e outros enfeites
  5. coloquialismos:"Trinta e poucos"
  6. frações:'um terço', 'dois quintos'
  7. nomes comuns:'uma dúzia', 'metade'

E provavelmente há mais advertências possíveis que ainda não foram listadas.Suponha que o algoritmo precise ser muito robusto e até mesmo compreender erros ortográficos.

Quais campos/artigos/estudos/algoritmos devo ler para aprender a escrever tudo isso?Onde está a informação?

PS:Meu analisador final deve compreender três idiomas diferentes: inglês, russo e hebraico.E talvez numa fase posterior mais idiomas sejam adicionados.O hebraico também tem números masculinos/femininos, como “um homem” e “uma mulher” têm um “um” diferente – “ehad” e “ahat”.O russo também tem algumas de suas complexidades.

O Google faz um ótimo trabalho nisso.Por exemplo:

http://www.google.com/search?q=dois+mil+e+um+cem+mais+cinco+dúzias+e+quatro+quintos+em+decimal

(o inverso também é possível http://www.google.com/search?q=999999999999+in+inglês)

Foi útil?

Solução

Eu estava brincando com um analisador PEG para fazer o que você queria (e posso postar isso como uma resposta separada mais tarde) quando percebi que há um algoritmo muito simples que faz um trabalho extraordinariamente bom com formas comuns de números em inglês, espanhol e Alemão, pelo menos.

Trabalhando com inglês, por exemplo, você precisa de um dicionário que mapeie palavras em valores da maneira óbvia:

"one" -> 1, "two" -> 2, ... "twenty" -> 20,
"dozen" -> 12, "score" -> 20, ...
"hundred" -> 100, "thousand" -> 1000, "million" -> 1000000

...e assim por diante

O algoritmo é apenas:

total = 0
prior = null
for each word w
    v <- value(w) or next if no value defined
    prior <- case
        when prior is null:       v
        when prior > v:     prior+v
        else                prior*v
        else
    if w in {thousand,million,billion,trillion...}
        total <- total + prior
        prior <- null
total = total + prior unless prior is null

Por exemplo, isso progride da seguinte forma:

total    prior      v     unconsumed string
    0      _              four score and seven 
                    4     score and seven 
    0      4              
                   20     and seven 
    0     80      
                    _     seven 
    0     80      
                    7 
    0     87      
   87

total    prior      v     unconsumed string
    0        _            two million four hundred twelve thousand eight hundred seven
                    2     million four hundred twelve thousand eight hundred seven
    0        2
                  1000000 four hundred twelve thousand eight hundred seven
2000000      _
                    4     hundred twelve thousand eight hundred seven
2000000      4
                    100   twelve thousand eight hundred seven
2000000    400
                    12    thousand eight hundred seven
2000000    412
                    1000  eight hundred seven
2000000  412000
                    1000  eight hundred seven
2412000     _
                      8   hundred seven
2412000     8
                     100  seven
2412000   800
                     7
2412000   807
2412807

E assim por diante.Não estou dizendo que seja perfeito, mas para um trabalho rápido e sujo funciona muito bem.


Abordando sua lista específica na edição:

  1. cardinal/nominal ou ordinal:"um" e "primeiro" - basta colocá-los no dicionário
  2. inglês/britânico:"quarenta"/"quarenta" -- idem
  3. centenas/milhares:2100 -> "vinte e cem" e também "dois mil e cem" -- funciona como está
  4. separadores:"mil e cinquenta e dois", mas também "mil e cento e cinquenta e dois" ou "mil e cento e cinquenta e dois" e outros enfeites - apenas defina "próxima palavra" como o prefixo mais longo que corresponde a uma palavra definida, ou até a próxima não-palavra, se nenhuma corresponder, para começar
  5. coloquialismos:"Trinta e poucos" -- funciona
  6. fragmentos:'um terço', 'dois quintos' - ah, ainda não...
  7. nomes comuns:'uma dúzia', 'metade' - funciona;você pode até fazer coisas como "meia dúzia"

O número 6 é o único para o qual não tenho uma resposta pronta, e isso se deve à ambiguidade entre ordinais e frações (pelo menos em inglês) somada ao fato de que minha última xícara de café foi muitos horas atrás.

Outras dicas

Não é um problema fácil e não conheço nenhuma biblioteca para fazer isso.Posso sentar e tentar escrever algo assim algum dia.Eu faria isso em Prolog, Java ou Haskell.Pelo que posso ver, existem vários problemas:

  • Tokenização:às vezes, os números são escritos mil cento e cinquenta e dois, mas já vi mil e cinquenta e dois ou mil e cento e cinquenta e dois e outros enfeites.Seria necessário realizar uma pesquisa sobre quais formulários estão realmente em uso.Isso pode ser especialmente complicado para o hebraico.
  • Erros de ortografia:isso não é tão difícil.Você tem uma quantidade limitada de palavras e um pouco de magia à distância de Levenshtein deve resolver.
  • Existem formulários alternativos, como você já mencionou.Isso inclui números ordinais/cardinais, bem como quarenta/quarenta e...
  • ...nomes comuns ou frases comumente usadas e NEs (entidades nomeadas).Você gostaria de extrair 30 da Guerra dos Trinta Anos ou 2 da Segunda Guerra Mundial?
  • Algarismos romanos também?
  • Coloquialismos, como “trinta e poucos” e “três euros e estilhaços”, que eu não saberia como tratar.

Se você estiver interessado nisso, posso tentar neste fim de semana.Minha ideia provavelmente é usar UIMA e tokenizar com ele, depois continuar a tokenizar/desambiguar e, finalmente, traduzir.Pode haver mais problemas, vamos ver se consigo pensar em coisas mais interessantes.

Desculpe, esta ainda não é uma resposta real, apenas uma extensão da sua pergunta.Avisarei você se encontrar/escrever algo.

A propósito, se você estiver interessado na semântica dos numerais, acabei de encontrar um artigo interessante por Friederike Moltmann, discutindo algumas questões relativas à interpretação lógica dos numerais.

Eu tenho um código que escrevi há um tempo: texto2num.Isso faz parte do que você deseja, exceto que não lida com números ordinais.Na verdade, não usei esse código para nada, então ele ainda não foi testado!

Utilize o Python padrão-pt biblioteca:

>>> from pattern.en import number
>>> number('two thousand fifty and a half') => 2050.5

Você deve ter em mente que a Europa e a América contam de forma diferente.

Padrão europeu:

One Thousand
One Million
One Thousand Millions (British also use Milliard)
One Billion
One Thousand Billions
One Trillion
One Thousand Trillions

Aqui é uma pequena referência sobre ele.


Uma maneira simples de ver a diferença é a seguinte:

(American counting Trillion) == (European counting Billion)

Os números ordinais não são aplicáveis ​​porque não podem ser unidos de maneira significativa com outros números no idioma (... pelo menos em inglês)

por exemplo.centésimo primeiro, onze segundos, etc...

No entanto, há outra advertência inglesa/americana com a palavra 'e'

ou seja

cento e um (Inglês) cento e um (americano)

Além disso, o uso de 'a' para significar um em inglês

mil = mil

... Por outro lado, a calculadora do Google faz um trabalho incrível nisso.

cento e três mil vezes a velocidade da luz

E até mesmo...

dois mil e cem mais uma dúzia

...que merda?!? uma pontuação mais uma dúzia em algarismos romanos

Aqui está uma solução extremamente robusta em Clojure.

AFAIK é uma abordagem de implementação única.

;----------------------------------------------------------------------
; numbers.clj
; written by: Mike Mattie codermattie@gmail.com
;----------------------------------------------------------------------
(ns operator.numbers
  (:use compojure.core)

  (:require
    [clojure.string     :as string] ))

(def number-word-table {
  "zero"          0
  "one"           1
  "two"           2
  "three"         3
  "four"          4
  "five"          5
  "six"           6
  "seven"         7
  "eight"         8
  "nine"          9
  "ten"           10
  "eleven"        11
  "twelve"        12
  "thirteen"      13
  "fourteen"      14
  "fifteen"       15
  "sixteen"       16
  "seventeen"     17
  "eighteen"      18
  "nineteen"      19
  "twenty"        20
  "thirty"        30
  "fourty"        40
  "fifty"         50
  "sixty"         60
  "seventy"       70
  "eighty"        80
  "ninety"        90
})

(def multiplier-word-table {
  "hundred"       100
  "thousand"      1000
})

(defn sum-words-to-number [ words ]
  (apply + (map (fn [ word ] (number-word-table word)) words)) )

; are you down with the sickness ?
(defn words-to-number [ words ]
  (let
    [ n           (count words)

      multipliers (filter (fn [x] (not (false? x))) (map-indexed
                                                      (fn [ i word ]
                                                        (if (contains? multiplier-word-table word)
                                                          (vector i (multiplier-word-table word))
                                                          false))
                                                      words) )

      x           (ref 0) ]

    (loop [ indices (reverse (conj (reverse multipliers) (vector n 1)))
            left    0
            combine + ]
      (let
        [ right (first indices) ]

        (dosync (alter x combine (* (if (> (- (first right) left) 0)
                                      (sum-words-to-number (subvec words left (first right)))
                                      1)
                                    (second right)) ))

        (when (> (count (rest indices)) 0)
          (recur (rest indices) (inc (first right))
            (if (= (inc (first right)) (first (second indices)))
              *
              +))) ) )
    @x ))

Aqui estão alguns exemplos

(operator.numbers/words-to-number ["six" "thousand" "five" "hundred" "twenty" "two"])
(operator.numbers/words-to-number ["fifty" "seven" "hundred"])
(operator.numbers/words-to-number ["hundred"])

Minha implementação LPC de alguns de seus requisitos (somente em inglês americano):

internal mapping inordinal = ([]);
internal mapping number = ([]);

#define Numbers ([\
    "zero"        : 0, \
    "one"         : 1, \
    "two"         : 2, \
    "three"       : 3, \
    "four"        : 4, \
    "five"        : 5, \
    "six"         : 6, \
    "seven"       : 7, \
    "eight"       : 8, \
    "nine"        : 9, \
    "ten"         : 10, \
    "eleven"      : 11, \
    "twelve"      : 12, \
    "thirteen"    : 13, \
    "fourteen"    : 14, \
    "fifteen"     : 15, \
    "sixteen"     : 16, \
    "seventeen"   : 17, \
    "eighteen"    : 18, \
    "nineteen"    : 19, \
    "twenty"      : 20, \
    "thirty"      : 30, \
    "forty"       : 40, \
    "fifty"       : 50, \
    "sixty"       : 60, \
    "seventy"     : 70, \
    "eighty"      : 80, \
    "ninety"      : 90, \
    "hundred"     : 100, \
    "thousand"    : 1000, \
    "million"     : 1000000, \
    "billion"     : 1000000000, \
])

#define Ordinals ([\
    "zeroth"        : 0, \
    "first"         : 1, \
    "second"        : 2, \
    "third"         : 3, \
    "fourth"        : 4, \
    "fifth"         : 5, \
    "sixth"         : 6, \
    "seventh"       : 7, \
    "eighth"        : 8, \
    "ninth"         : 9, \
    "tenth"         : 10, \
    "eleventh"      : 11, \
    "twelfth"       : 12, \
    "thirteenth"    : 13, \
    "fourteenth"    : 14, \
    "fifteenth"     : 15, \
    "sixteenth"     : 16, \
    "seventeenth"   : 17, \
    "eighteenth"    : 18, \
    "nineteenth"    : 19, \
    "twentieth"     : 20, \
    "thirtieth"     : 30, \
    "fortieth"      : 40, \
    "fiftieth"      : 50, \
    "sixtieth"      : 60, \
    "seventieth"    : 70, \
    "eightieth"     : 80, \
    "ninetieth"     : 90, \
    "hundredth"     : 100, \
    "thousandth"    : 1000, \
    "millionth"     : 1000000, \
    "billionth"     : 1000000000, \
])

varargs int denumerical(string num, status ordinal) {
    if(ordinal) {
        if(member(inordinal, num))
            return inordinal[num];
    } else {
        if(member(number, num))
            return number[num];
    }
    int sign = 1;
    int total = 0;
    int sub = 0;
    int value;
    string array parts = regexplode(num, " |-");
    if(sizeof(parts) >= 2 && parts[0] == "" && parts[1] == "-")
        sign = -1;
    for(int ix = 0, int iix = sizeof(parts); ix < iix; ix++) {
        string part = parts[ix];
        switch(part) {
        case "negative" :
        case "minus"    :
            sign = -1;
            continue;
        case ""         :
            continue;
        }
        if(ordinal && ix == iix - 1) {
            if(part[0] >= '0' && part[0] <= '9' && ends_with(part, "th"))
                value = to_int(part[..<3]);
            else if(member(Ordinals, part))
                value = Ordinals[part];
            else
                continue;
        } else {
            if(part[0] >= '0' && part[0] <= '9')
                value = to_int(part);
            else if(member(Numbers, part))
                value = Numbers[part];
            else
                continue;
        }
        if(value < 0) {
            sign = -1;
            value = - value;
        }
        if(value < 10) {
            if(sub >= 1000) {
                total += sub;
                sub = value;
            } else {
                sub += value;
            }
        } else if(value < 100) {
            if(sub < 10) {
                sub = 100 * sub + value;
            } else if(sub >= 1000) {
                total += sub;
                sub = value;
            } else {
                sub *= value;
            }
        } else if(value < sub) {
            total += sub;
            sub = value;
        } else if(sub == 0) {
            sub = value;
        } else {
            sub *= value;
        }
    }
    total += sub;
    return sign * total;
}

Bem, cheguei tarde demais na resposta para essa pergunta, mas estava trabalhando em um pequeno cenário de teste que parece ter funcionado muito bem para mim.Usei uma expressão regular (simples, mas feia e grande) para localizar todas as palavras para mim.A expressão é a seguinte:

(?<Value>(?:zero)|(?:one|first)|(?:two|second)|(?:three|third)|(?:four|fourth)|
(?:five|fifth)|(?:six|sixth)|(?:seven|seventh)|(?:eight|eighth)|(?:nine|ninth)|
(?:ten|tenth)|(?:eleven|eleventh)|(?:twelve|twelfth)|(?:thirteen|thirteenth)|
(?:fourteen|fourteenth)|(?:fifteen|fifteenth)|(?:sixteen|sixteenth)|
(?:seventeen|seventeenth)|(?:eighteen|eighteenth)|(?:nineteen|nineteenth)|
(?:twenty|twentieth)|(?:thirty|thirtieth)|(?:forty|fortieth)|(?:fifty|fiftieth)|
(?:sixty|sixtieth)|(?:seventy|seventieth)|(?:eighty|eightieth)|(?:ninety|ninetieth)|
(?<Magnitude>(?:hundred|hundredth)|(?:thousand|thousandth)|(?:million|millionth)|
(?:billion|billionth)))

Mostrado aqui com quebras de linha para fins de formatação.

De qualquer forma, meu método foi executar este RegEx com uma biblioteca como PCRE e depois ler as correspondências nomeadas.E funcionou em todos os diferentes exemplos listados nesta questão, menos os tipos "Metade", já que não os adicionei, mas como você pode ver, não seria difícil fazê-lo.Isso aborda muitos problemas.Por exemplo, aborda os seguintes itens da pergunta original e outras respostas:

  1. cardinal/nominal ou ordinal:"um" e "primeiro"
  2. erros ortográficos comuns:"quarenta"/"quarenta" (Observe que isso não aborda isso EXPLICITAMENTE, isso seria algo que você gostaria de fazer antes de passar a string para este analisador.Este analisador vê este exemplo como "QUATRO"...)
  3. centenas/milhares:2100 -> “vinte e cem” e também “dois mil e cem”
  4. separadores:"mil e cinquenta e dois", mas também "mil e cento e cinquenta e dois" ou "mil e cento e cinquenta e dois" e outros enfeites
  5. coloquialismos:"trinta e poucos" (Isso também não é TOTALMENTE abordado, pois o que É "alguma coisa"?Bem, este código encontra esse número simplesmente como "30").**

Agora, em vez de armazenar esse monstro de expressão regular em sua fonte, eu estava pensando em construir esse RegEx em tempo de execução, usando algo como o seguinte:

char *ones[] = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve",
  "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"};
char *tens[] = {"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"};
char *ordinalones[] = { "", "first", "second", "third", "fourth", "fifth", "", "", "", "", "", "", "twelfth" };
char *ordinaltens[] = { "", "", "twentieth", "thirtieth", "fortieth", "fiftieth", "sixtieth", "seventieth", "eightieth", "ninetieth" };
and so on...

A parte fácil aqui é que armazenamos apenas as palavras que importam.No caso do SEXTO, você notará que não há uma entrada para ele, porque é apenas o número normal com o TH afixado...Mas alguns como TWELVE precisam de atenção diferente.

Ok, agora temos o código para construir nosso (feio) RegEx, agora apenas o executamos em nossas strings numéricas.

Uma coisa que eu recomendaria é filtrar ou comer a palavra "AND".Não é necessário e só leva a outros problemas.

Então, o que você vai querer fazer é configurar uma função que passe as correspondências nomeadas para "Magnitude" em uma função que analise todos os valores de magnitude possíveis e multiplique seu resultado atual por esse valor de magnitude.Em seguida, você cria uma função que analisa as correspondências nomeadas de "Valor" e retorna um int (ou o que você estiver usando), com base no valor descoberto lá.

Todas as correspondências de VALOR são ADICIONADAS ao seu resultado, enquanto as correspondências de magnitude multiplicam o resultado pelo valor magnético.Então, duzentos e cinquenta mil se tornam "2", depois "2 * 100", depois "200 + 50", depois "250 * 1000", terminando com 250.000...

Só por diversão, escrevi uma versão em vbScript e funcionou muito bem com todos os exemplos fornecidos.Agora, ele não suporta correspondências nomeadas, então tive que trabalhar um pouco mais para conseguir o resultado correto, mas consegui.Resumindo, se for uma correspondência "VALUE", adicione-a ao seu acumulador.Se for uma correspondência de magnitude, multiplique seu acumulador por 100, 1000, 1000000, 1000000000, etc...Isso fornecerá alguns resultados surpreendentes, e tudo o que você precisa fazer para ajustar coisas como "metade" é adicioná-los ao seu RegEx, colocar um marcador de código para eles e lidar com eles.

Bem, espero que este post ajude ALGUÉM por aí.Se alguém quiser, posso postar em pseudocódigo vbScript que usei para testar isso, porém, não é um código bonito e NÃO é um código de produção.

Se eu puder..Qual é o idioma final em que isso será escrito?C++ ou algo parecido com uma linguagem de script?A fonte de Greg Hewgill contribuirá muito para ajudar a entender como tudo isso acontece.

Deixe-me saber se posso ajudar em alguma outra coisa.Desculpe, só sei inglês/americano, então não posso ajudá-lo com os outros idiomas.

Eu estava convertendo declarações de edição ordinal de livros modernos (por exemplo,"2ª edição", "Editio quarta") para números inteiros e precisava de suporte para ordinais 1-100 em inglês e ordinais 1-10 em algumas línguas românicas.Aqui está o que descobri em Python:

def get_data_mapping():
  data_mapping = {
    "1st": 1,
    "2nd": 2,
    "3rd": 3,

    "tenth": 10,
    "eleventh": 11,
    "twelfth": 12,
    "thirteenth": 13,
    "fourteenth": 14,
    "fifteenth": 15,
    "sixteenth": 16,
    "seventeenth": 17,
    "eighteenth": 18,
    "nineteenth": 19,
    "twentieth": 20,

    "new": 2,
    "newly": 2,
    "nova": 2,
    "nouvelle": 2,
    "altera": 2,
    "andere": 2,

    # latin
    "primus": 1,
    "secunda": 2,
    "tertia": 3,
    "quarta": 4,
    "quinta": 5,
    "sexta": 6,
    "septima": 7,
    "octava": 8,
    "nona": 9,
    "decima": 10,

    # italian
    "primo": 1,
    "secondo": 2,
    "terzo": 3,
    "quarto": 4,
    "quinto": 5,
    "sesto": 6,
    "settimo": 7,
    "ottavo": 8,
    "nono": 9,
    "decimo": 10,

    # french
    "premier": 1,
    "deuxième": 2,
    "troisième": 3,
    "quatrième": 4,
    "cinquième": 5,
    "sixième": 6,
    "septième": 7,
    "huitième": 8,
    "neuvième": 9,
    "dixième": 10,

    # spanish
    "primero": 1,
    "segundo": 2,
    "tercero": 3,
    "cuarto": 4,
    "quinto": 5,
    "sexto": 6,
    "septimo": 7,
    "octavo": 8,
    "noveno": 9,
    "decimo": 10
  }

  # create 4th, 5th, ... 20th
  for i in xrange(16):
    data_mapping[str(4+i) + "th"] = 4+i

  # create 21st, 22nd, ... 99th
  for i in xrange(79):
    last_char = str(i)[-1]

    if last_char == "0":
      data_mapping[str(20+i) + "th"] = 20+i

    elif last_char == "1":
      data_mapping[str(20+i) + "st"] = 20+i

    elif last_char == "2":
      data_mapping[str(20+i) + "nd"] = 20+i

    elif last_char == "3":
      data_mapping[str(20+i) + "rd"] = 20+i

    else:
      data_mapping[str(20+i) + "th"] = 20+i

  ordinals = [
    "first", "second", "third", 
    "fourth", "fifth", "sixth", 
    "seventh", "eighth", "ninth"
  ]

  # create first, second ... ninth
  for c, i in enumerate(ordinals):
    data_mapping[i] = c+1

  # create twenty-first, twenty-second ... ninty-ninth
  for ci, i in enumerate([
    "twenty", "thirty", "forty", 
    "fifty", "sixty", "seventy", 
    "eighty", "ninety"
  ]):
    for cj, j in enumerate(ordinals):
      data_mapping[i + "-" + j] = 20 + (ci*10) + (cj+1)
    data_mapping[i.replace("y", "ieth")] = 20 + (ci*10)

  return data_mapping

Tentar

  1. Abra uma solicitação HTTP para "http://www.google.com/search?q=" + número + "+em+decimal".

  2. Analise o resultado para o seu número.

  3. Armazene em cache os pares número/resultado para controlar as solicitações ao longo do tempo.

Um lugar para começar a procurar é o gnu get_date biblioteca, que pode analisar praticamente qualquer Data textual em inglês em um carimbo de data/hora.Embora não seja exatamente o que você está procurando, a solução para um problema semelhante pode fornecer muitas pistas úteis.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top