summer_tree_home

Check iOでPython3をマスターするぜっ

Striped Words (O'Reilly) - ストライプワード

どんな問題?

Striped Words
http://www.checkio.org/mission/striped-words/

テキスト中に、ストライプワード(母音と子音が交互に並んでいる単語)が何個でてくるか数えよ。
単語は、スペースと句読点で区切られている。
数字や、数字と文字の組み合わせは、単語ではないものとする。
一文字の単語は、ストライプワードとしてカウントしないこと。

引数は、テキスト(文字列)。
戻り値は、ストライプワードの数。

例題:

checkio("My name is ...") == 3
checkio("Hello world") == 0
checkio("A quantity of striped words.") == 1, "Only of"
checkio("Dog,cat,mouse,bird.Human.") == 3

どうやって解く?

  1. テキストをスペースと句読点で分割する。
  2. 数字や数字を含む単語を除外する。
  3. ストライプワードかどうかをチェックする。

という流れでいけそうだ。

句読点の定義は?

そもそも句読点(punctuation)ってどこまで含まれるのだろう?例題に出てくるのは「.,」だが、「:;?!」なども句読点になるのだろうか?
punctuationで検索してみたら、Pythonでは string.punctuation として定義されているようだ。また、string.whitespace というのもあったので、この2つを使うことにする。

1.テキストをスペースと句読点で分割する。

str.split() では区切り文字を一種類しか指定できないので、複数の区切り文字を指定して文字列を分割するのは少しやっかい。

区切り文字を1種類にまとめてからsplitする方法がある。例えば、区切り文字が「 」「.」「,」の3種類なら、「 」と「.」を「,」に置換してから、「,」でsplitする。

s.replace(' ', ',').replace('.', ',').split(',')

しかし、区切り文字が増えてくるとちょっと面倒だ。今回は、正規表現を使うことにした。

正規表現はよく知らないので、ググってみると、re.split() でいいみたいだ。

def split_with_separators1(s, separators):
    pattern = '|'.join(re.escape(s) for s in separators)
    return re.split(pattern, s)

ただ、これだと、text = "My name is ..." のときに

['My', 'name', 'is', '', '', '', '']

となってしまう。
''を含まないようにするなら、re.findall()を使う方法もある。

def split_with_separators2(s, separators):
    return re.findall("[^" + re.escape(separators) + "]+", s)

「区切り文字以外から成る一文字以上の文字列をすべて取得」という意味になる。これだと、結果は

['My', 'name', 'is']

となる。今回はこっちを使おうかな。
(追記)
この関数では、1文字の区切り文字を複数指定することになる。複数文字の区切り文字を複数指定することはできない。

2.数字や数字を含む単語を除外する。

普通に一文字ずつ調べてみる。

def contains_digits1(s):
    return any(c in string.digits for c in word)

正規表現を使う方法も調べてみた。

def contains_digits2(s):
    return re.search('\d', s) is not None

3.ストライプワードかどうかをチェックする。

単語の奇数番目の文字と、偶数番目の文字をチェックする。「偶数番目がすべて母音 かつ 奇数番目がすべて子音」または「偶数番目がすべて子音 かつ 奇数番目がすべて母音」であればいい。

VOWELS = set("AEIOUY")
CONSONANTS = set("BCDFGHJKLMNPQRSTVWXZ")

def is_striped_word(s):
    even, odd = set(s[::2].upper()), set(s[1::2].upper())
    return (even <= VOWELS and odd <= CONSONANTS) or (odd <= VOWELS and even <= CONSONANTS)

ただ、これだと、sが一文字のときは、evenまたはoddが{}となって、戻り値がTrueになってしまうので、

def is_striped_word(s):
    even, odd = set(s[::2].upper()), set(s[1::2].upper())
    if len(even) == 0 or len(odd) == 0:
        return False
    return (even <= VOWELS and odd <= CONSONANTS) or (odd <= VOWELS and even <= CONSONANTS)

としておいた。

(注)公開後に気付いたが、単に

    if len(s) <= 1: 
        return False

で良かった。orz

まとめ

import string
import re
 
VOWELS = set("AEIOUY")
CONSONANTS = set("BCDFGHJKLMNPQRSTVWXZ")
 
def split_with_separators(s, separators):
    return re.findall("[^" + re.escape(separators) + "]+", s)
 
def contains_digits(s):
    return re.search('\d', s) is not None
 
def is_striped_word(s):
    even, odd = set(s[::2].upper()), set(s[1::2].upper())
    if len(even) == 0 or len(odd) == 0:
        return False
    return (even <= VOWELS and odd <= CONSONANTS) or (odd <= VOWELS and even <= CONSONANTS)
 
def checkio(text):
    words = split_with_separators(text, string.whitespace + string.punctuation)
    return len([w for w in words if not contains_digits(w) and is_striped_word(w)])

http://www.checkio.org/mission/striped-words/publications/natsuki/python-3/first/

見直すと、あれこれ修正したい部分が出てくるけど、、、まあいいか。

ところで、「複数の区切り文字を指定して文字列を分割できるメソッド」は、Python標準で対応してほしいなぁ。