轨道
/
Elixir
Elixir
/
练习
/
日期解析器
日期解析器

日期解析器

学习练习

简介

正则表达式

正则表达式(regex)是 Elixir 中处理字符串的强大工具。Elixir 中的正则表达式遵循PCRE规范(Perl Compatible Regular Expressions)。表示正则表达式含义的字符串模式会先被编译,然后用于匹配整个字符串或其中的一部分。

在 Elixir 中,创建正则表达式最常见的方式是使用~r sigil。Sigil 为 Elixir 中的常见任务提供了_语法糖_式的快捷写法。要匹配一个_字符串字面量_,我们可以把字符串本身作为跟在 sigil 后面的模式。

~r/test/

=~/2运算符用于对字符串执行正则匹配并返回boolean结果,十分有用。

"this is a test" =~ ~r/test/
# => true

关于使用 sigil,有两点说明:

  • 根据你的需求,可以使用许多不同的分隔符,而不只是/
  • 字符串模式本身已经是_转义_过的;当你不用正则表达式、而是把模式写成字符串时,就需要对反斜杠(\)进行_转义_

字符类

用方括号[]匹配一个字符范围,就定义了一个_字符类_。它会从该类包含的字符中匹配任意一个字符。你也可以指定像a-z这样的字符范围,只要起点和终点代表一段连续的码点。

regex = ~r/[a-z][ADKZ][0-9][!?]/
"jZ5!" =~ regex
# => true
"jB5?" =~ regex
# => false

_简写字符类_能让模式更简洁。例如:

  • \d是[0-9]的简写(任意数字)
  • \w是[A-Za-z0-9_]的简写(任意“单词”字符)
  • \s是[ \t\r\n\f]的简写(任意空白字符)

当_简写字符类_用在 sigil 之外时,必须进行转义:"\\d"

选择

_选择_使用|作为特殊字符,表示匹配其中之一。

regex = ~r/cat|bat/
"bat" =~ regex
# => true
"cat" =~ regex
# => true

量词

_量词_允许正则表达式中出现重复的模式。它们作用于量词前面的分组。

  • {N, M},其中N是重复次数的最小值,M是最大值
  • {N,}匹配N次或更多次重复
    • {0,}也可以写成*:匹配零次或更多次重复
    • {1,}也可以写成+:匹配一次或更多次重复
  • {,N}匹配最多N次重复

分组

圆括号()用来表示_分组_和_捕获_。在某些情况下,分组还可以被_捕获_,以便返回使用。在 Elixir 中,捕获可以命名,也可以不命名。捕获的命名方式是在左括号后面追加?<name>。分组会作为一个整体起作用,比如在它后面跟有_量词_时。

regex = ~r/(h)at/
Regex.replace(regex, "hat", "\\1op")
# => "hop"

regex = ~r/(?<letter_b>b)/
Regex.scan(regex, "blueberry", capture: :all_names)
# => [["b"], ["b"]]

锚点

_锚点_用于把正则表达式固定到待匹配字符串的开头或结尾:

  • ^锚定到字符串的开头
  • $锚定到字符串的结尾

插值

由于~r是"pattern" |> Regex.escape() |> Regex.compile!()的快捷写法,你也可以使用字符串插值来动态构建正则表达式模式:

anchor = "$"
regex = ~r/end of the line#{anchor}/
"end of the line?" =~ regex
# => false
"end of the line" =~ regex
# => true

说明

你的任务是编写一个用于接收事件的服务。每个事件都带有一个日期,但你注意到,提交到你服务端点的日期有 3 种不同的格式:

  • "01/01/1970"
  • "January 1, 1970"
  • "Thursday, January 1, 1970"

你能看出它们之间有一些相似之处,于是决定编写一些可组合的正则表达式模式。

1. 匹配日期中的日、月和年

实现 day/0、month/0 和 year/0,返回一个字符串模式。该模式编译后,能够匹配"01/01/1970"(dd/mm/yyyy)中的数字部分。日和月可能以1或01的形式出现(左侧用零补齐)。

"31" =~ DateParser.day() |> Regex.compile!()
# => true
"12" =~ DateParser.month() |> Regex.compile!()
# => true
"1970" =~ DateParser.year() |> Regex.compile!()
# => true

2. 匹配星期和月份

实现 day_names/0 和 month_names/0,返回一个字符串模式。该模式编译后,能够分别匹配以名称表示的星期和以名称表示的月份。

"Tuesday" =~ DateParser.day_names() |> Regex.compile!()
# => true
"June" =~ DateParser.month_names() |> Regex.compile!()
# => true

3. 捕获日、月和年

实现 capture_day/0、capture_month/0、capture_year/0、capture_day_name/0、capture_month_name/0,返回一个字符串模式,把相应的部分捕获为名称:"day"、"month"、"year"、"day_name"、"month_name"

DateParser.capture_month_name()
|> Regex.compile!()
|> Regex.named_captures("December")

# => %{"month_name" => "December"}

4. 组合这些捕获,以捕获完整日期

实现 capture_numeric_date/0、capture_month_name_date() 和 capture_day_month_name_date/0,返回一个字符串模式,按照对应的日期格式捕获第 3 部分中的各个部分:

  • 数字日期 - "01/01/1970"
  • 月份名称日期 - "January 1, 1970"
  • 星期加月份名称日期 - "Thursday, January 1, 1970"
DateParser.capture_numeric_date()
|> Regex.compile!()
|> Regex.named_captures("01/01/1970")

# => %{"day" => "01", "month" => "01", "year" => "1970"}

5. 缩小捕获范围,使其只匹配日期

实现 match_numeric_date/0、match_month_name_date/0 和 match_day_month_name_date/0,返回一个已编译的正则表达式。它只匹配日期,同时也能捕获各个部分。

"Thursday, January 1, 1970 was the Unix epoch." =~ DateParser.match_day_month_name_date()
# => false
"Thursday, January 1, 1970" =~ DateParser.match_day_month_name_date()
# => true

DateParser.match_day_month_name_date()
|> Regex.named_captures("Thursday, January 1, 1970")

# => %{
#     "day" => "1",
#     "day_name" => "Thursday",
#     "month_name" => "January",
#     "year" => "1970"
#   }
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Elixir Exercism

准备好开始 日期解析器 了吗?

注册 Exercism,借助 58 个概念168 个练习 和真人导师指导,学习并掌握 Elixir,全部免费。