Repository navigation
Expand file tree
/
Copy pathfilepattern.jl
More file actions
153 lines (130 loc) · 5.77 KB
/
Copy pathfilepattern.jl
File metadata and controls
153 lines (130 loc) · 5.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
using Dates
# A `{}` date format. `Dates` renders every field but `j`, so the body splits into `DateFormat`
# chunks and the widths of `j` runs.
struct DatePart
body::String # As written: what `show` reproduces.
tokens::Vector{Union{DateFormat,Int}}
stop::Bool # `{t1:…}`: the step's end rather than its start.
end
DatePart(body::AbstractString, stop::Bool=false) = DatePart(body, _tokens(body), stop)
# A keyword placeholder; `case` is 'U'/'L' for `{name|U}`/`{name|L}`, '-' for `{name}`.
struct KeyPart
name::Symbol
case::Char
end
_apply(k::KeyPart, v) = (s = string(v); k.case == 'U' ? uppercase(s) : k.case == 'L' ? lowercase(s) : s)
_kpstr(k::KeyPart) = k.case == '-' ? string(k.name) : string(k.name, '|', k.case)
const _Part = Union{String,DatePart,KeyPart}
"""
FilePattern(pattern; cadence=Day(1), kw...)
A template for the names of an archive's files, one per `cadence`.
Text in `{}` is a placeholder: `{t:fmt}` renders the step's start through the `Dates` format `fmt`
(`{t:mm-dd}`, `{t:yyyyjjj}` with `j` for day of year) and `{t1:fmt}` the last instant it covers, for
archives naming both ends. A name without a format is a keyword; `{name|U}` and `{name|L}` render
its value upper- or lowercased. Everything else is literal.
Keywords may be filled at construction, when specializing, or at the call; every one must be filled
before a name is rendered. Filling a keyword with `"*"` leaves a wildcard for [`resolve_url`](@ref)
to match against the archive's listing.
```jldoctest
julia> using Dates
julia> FilePattern("g13_xrs_1m_{t:yyyymm}01_{t1:yyyymmdd}.nc"; cadence=Month(1))(Date(2012, 2))
"g13_xrs_1m_20120201_20120229.nc"
```
"""
struct FilePattern{P<:Period}
parts::Vector{_Part}
cadence::P
end
function FilePattern(pattern::AbstractString; cadence=Day(1), kw...)
parts = _parse_pattern(pattern)
_repeats(parts, cadence) &&
throw(ArgumentError("consecutive $cadence steps name the same file in $(repr(pattern)); its date fields cannot tell them apart"))
return FilePattern(parts, cadence)(; kw...)
end
_fill_keys(parts, fills) =
_fuse(x isa KeyPart && haskey(fills, x.name) ? _apply(x, fills[x.name]) : x for x in parts)
_bind(p::FilePattern, fills) = FilePattern(_fill_keys(p.parts, fills), p.cadence)
(p::FilePattern)(; kw...) = _bind(p, kw)
# Fuse adjacent literals so rendering walks fewer parts.
function _fuse(parts)
out = _Part[]
for x in parts
last = isempty(out) ? nothing : out[end]
x isa String && last isa String ? (out[end] = last * x) : push!(out, x)
end
return out
end
"""Fill keywords and `t`, giving a URL."""
(p::FilePattern)(t; kw...) = isempty(kw) ? _render(p, t) : p(; kw...)(t)
function _tokens(body)
ts = Union{DateFormat,Int}[]
i = firstindex(body)
for m in eachmatch(r"j+", body)
m.offset > i && push!(ts, DateFormat(body[i:prevind(body, m.offset)]))
push!(ts, length(m.match))
i = m.offset + ncodeunits(m.match)
end
i <= lastindex(body) && push!(ts, DateFormat(body[i:end]))
return ts
end
function _format(io::IO, d::DatePart, dt)
for tok in d.tokens
tok isa Int ? print(io, string(dayofyear(dt); pad=tok)) : Dates.format(io, dt, tok)
end
end
# Whether one step's name equals the next's, which would make a whole range fetch a single file.
function _repeats(parts, cadence)
dates = [x for x in parts if x isa DatePart]
isempty(dates) && return false
at(t) = sprint(io -> foreach(d -> _format(io, d, d.stop ? t + cadence - Millisecond(1) : t), dates))
t = DateTime(2001) # Aligned to every usual cadence; a Monday, so `Week` steps land on boundaries.
return at(t) == at(t + cadence)
end
function _parse_pattern(s::AbstractString)
parts = _Part[]
i = firstindex(s)
while true
j = findnext('{', s, i)
if isnothing(j)
i <= lastindex(s) && push!(parts, s[i:end])
return parts
end
k = findnext('}', s, j)
isnothing(k) && throw(ArgumentError("unterminated '{' in pattern $(repr(s))"))
j > i && push!(parts, s[i:prevind(s, j)])
push!(parts, _placeholder(s[nextind(s, j):prevind(s, k)], s))
i = nextind(s, k)
end
end
function _placeholder(body, s)
isempty(body) && throw(ArgumentError("empty placeholder '{}' in pattern $(repr(s))"))
i = findfirst(':', body)
if isnothing(i)
j = findfirst('|', body)
isnothing(j) && return KeyPart(Symbol(body), '-')
case = body[nextind(body, j):end]
case in ("U", "L") ||
throw(ArgumentError("unknown case modifier in {$body} of pattern $(repr(s)); use |U or |L"))
return KeyPart(Symbol(body[1:prevind(body, j)]), only(case))
end
name = body[1:prevind(body, i)]
name in ("t", "t1") ||
throw(ArgumentError("unknown time {$name} in pattern $(repr(s)); use {t:…} or {t1:…}"))
spec = body[nextind(body, i):end]
isempty(spec) && throw(ArgumentError("empty date format in {$body} of pattern $(repr(s))"))
return DatePart(spec, name == "t1")
end
function _render(p::FilePattern, t)
start = floor(DateTime(_time(t)), p.cadence)
stop = start + p.cadence - Millisecond(1) # `{t1:…}`: a monthly file reads `…_20101201_20101231.nc`.
io = IOBuffer()
for x in p.parts
x isa String ? write(io, x) :
x isa DatePart ? _format(io, x, x.stop ? stop : start) :
throw(ArgumentError("unfilled placeholder {$(_kpstr(x))} in pattern $(repr(_pattern(p)))"))
end
return String(take!(io))
end
_pattern(p::FilePattern) = _pattern(p.parts)
_pattern(parts) = join(x isa String ? x : x isa DatePart ? "{$(x.stop ? "t1" : "t"):$(x.body)}" : "{$(_kpstr(x))}" for x in parts)
Base.show(io::IO, p::FilePattern) = print(io, "FilePattern(", repr(_pattern(p)), p.cadence == Day(1) ? "" : "; cadence = $(p.cadence)", ")")