Skip to content

Commit 4741516

Browse files
sebastianpechDatseris
authored andcommitted
Add function for reverse engineering savename (#65)
* Add function for reverse engineering savename * Switch to dirname and basename instead of splitpath * Remove isnothing for compat. with julia 1.0 * Fix unit tests * Remove leftover kw def from docstring * Change parsing scheme to underscore in key instead of value * Add check for limiting connector to a single character * Switch to joinpath in test to support windows * documentation related updates * ops, update tests as well
1 parent 6b7d785 commit 4741516

File tree

6 files changed

+182
-1
lines changed

6 files changed

+182
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ docs/build
55
docs/data
66
Manifest.toml
77
test/testdir
8+
src/update*

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# 0.6.0
22
* **[BREAKING]** Reworked the way the functions `projectdir` and derivatives work (#47, #64, #66). Now `projectdir(args...)` uses `joinpath` to connect arguments. None of the functions like `projectdir` and derivatives now end in `/` as well, to ensure more stability and motivate users to use `joinpath` or the new functionality of `projectdir(args...)` instead of using string multiplication `*`.
3+
* New function `parse_savename` that attempts to reverse engineer the result of `savename`.
4+
35
# 0.5.1
46
* Improvements to `.gitignore` (#55 , #54)
57
# 0.5.0

docs/src/name.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,8 @@ DrWatson.default_expand
3838

3939
See [Real World Examples](@ref) for an example of customizing `savename`.
4040
Specifically, have a look at [`savename` and nested containers](@ref) for a way to
41+
42+
## Reverse-engineering `savename`
43+
```@docs
44+
parse_savename
45+
```

src/DrWatson.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ This will likely break usage of e.g. `datadir` that used `*`, like it was
4343
suggested in the old (unhealthy) documentation. We are very sorry
4444
for this inconvenience!
4545
46+
[NEW] New funtion `parse_savename` that reverse-engineers the output
47+
of `savename`!
4648
\n
4749
"""; color = :light_magenta)
4850
touch(joinpath(@__DIR__, update_name))

src/naming.jl

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export savename, @dict, @ntuple, @strdict
1+
export savename, @dict, @ntuple, @strdict, parse_savename
22
export ntuple2dict, dict2ntuple
33

44
"""
@@ -26,6 +26,7 @@ See [`default_prefix`](@ref) for more.
2626
2727
`savename` can be very conveniently combined with
2828
[`@dict`](@ref) or [`@ntuple`](@ref).
29+
See also [`parse_savename`](@ref).
2930
3031
## Keywords
3132
* `allowedtypes = default_allowed(c)` : Only values of type subtyping
@@ -250,3 +251,95 @@ end
250251
function dict2ntuple(dict::Dict{Symbol, T}) where T
251252
NamedTuple{Tuple(keys(dict))}(values(dict))
252253
end
254+
255+
256+
"""
257+
parse_savename(filename::AbstractString; kwargs...)
258+
Try to convert a shorthand name produced with [`savename`](@ref) into a dictionary
259+
containing the parameters and their values, a prefix and suffix string.
260+
Return `prefix, parameters, suffix`.
261+
262+
Parsing the key-value parts of `filename` is performed under the assumption that the value
263+
is delimited by `=` and the closest `connector`. This allows the user to have `connector`
264+
(eg. `_`) in a key name (variable name) but not in the value part.
265+
266+
## Keywords
267+
* `connector = "_"` : string used to connect the various entries.
268+
* `parsetypes = (Int, Float64)` : tuple used to define the types which should
269+
be tried when parsing the values given in `filename`. Fallback is `String`.
270+
"""
271+
function parse_savename(filename::AbstractString;
272+
parsetypes = (Int, Float64),
273+
connector::AbstractString = "_")
274+
length(connector) == 1 || error(
275+
"Cannot parse savenames where the 'connector'"*
276+
" string consists of more than one character.")
277+
278+
# Prefix can be anything, so it might also contain a folder which's
279+
# name was generated using savename. Therefore first the path is split
280+
# into folders and filename.
281+
prefix_part, savename_part = dirname(filename),basename(filename)
282+
# Extract the suffix. A suffix is identified by searching for the last "."
283+
# after the last "=".
284+
last_eq = findlast("=",savename_part)
285+
last_dot = findlast(".",savename_part)
286+
if last_dot == nothing || last_eq > last_dot
287+
# if no dot is after the last "="
288+
# there is no suffix
289+
name, suffix = savename_part,""
290+
else
291+
# Check if the last dot is part of a float number by parsing it as Int
292+
if tryparse(Int,savename_part[first(last_dot)+1:end]) == nothing
293+
# no int, so the part after the last dot is the suffix
294+
name, suffix = savename_part[1:first(last_dot)-1], savename_part[first(last_dot)+1:end]
295+
else
296+
# no suffix, because the dot just denotes the decimal places.
297+
name, suffix = savename_part, ""
298+
end
299+
end
300+
# Extract the prefix by searching for the first connector that comes before
301+
# an "=".
302+
first_eq = findfirst("=",name)
303+
first_connector = findfirst(connector,name)
304+
if first_connector == nothing || first(first_eq) < first(first_connector)
305+
prefix, _parameters = "", name
306+
else
307+
# There is a connector symbol before, so there might be a connector.
308+
# Of course the connector symbol could also be part of the variable name.
309+
prefix, _parameters = name[1:first(first_connector)-1], name[first(first_connector)+1:end]
310+
end
311+
# Add leading directory back to prefix
312+
prefix = joinpath(prefix_part,prefix)
313+
parameters = Dict{String,Any}()
314+
# Regex that matches smalles possible range between = and connector.
315+
# This way it is possible to corretly match something where the
316+
# connector ("_") was used as a variable name.
317+
# var_with_undersocore_1=foo_var=123.32_var_name_with_underscore=4.4
318+
# var_with_undersocore_1[=foo_]var[=123.32_]var_name_with_underscore=4.4
319+
name_seperator = Regex("=[^$connector]+$connector")
320+
c_idx = 1
321+
while (next_range = findnext(name_seperator,_parameters,c_idx)) != nothing
322+
equal_sign, end_of_value = first(next_range), last(next_range)-1
323+
parameters[_parameters[c_idx:equal_sign-1]] =
324+
parse_from_savename_value(parsetypes,_parameters[equal_sign+1:end_of_value])
325+
c_idx = end_of_value+2
326+
end
327+
# The last = cannot be followed by a connector, so it's not captured by the regex.
328+
equal_sign = findnext("=",_parameters,c_idx) |> first
329+
parameters[_parameters[c_idx:equal_sign-1]] =
330+
parse_from_savename_value(parsetypes,_parameters[equal_sign+1:end])
331+
return prefix,parameters,suffix
332+
end
333+
334+
"""
335+
parse_from_savename_value(types,str)
336+
Try parsing `str` with the types given in `types`. The first working parse is returned.
337+
Fallback is `String` ie. `str` is returned.
338+
"""
339+
function parse_from_savename_value(types::NTuple{N,<:Type},str::AbstractString) where N
340+
for t in types
341+
res = tryparse(t,str)
342+
res == nothing || return res
343+
end
344+
return str
345+
end

test/naming_tests.jl

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,81 @@ s = savename(e; allowedtypes = (Any,), expand = ["c"])
5858
@test ')' s
5959
@test occursin("a=3", s)
6060
@test occursin("b=4", s)
61+
62+
# Tests for parse_savename
63+
64+
function test_convert(prefix::AbstractString,c;kwargs...)
65+
name = savename(prefix,c;kwargs...)
66+
_prefix, _b, _suffix = DrWatson.parse_savename(name)
67+
dicts_equal(_b,c) && _prefix == prefix && _suffix == ""
68+
end
69+
70+
function test_convert(c,suffix::AbstractString;kwargs...)
71+
name = savename(c,suffix;kwargs...)
72+
_prefix, _b, _suffix = DrWatson.parse_savename(name)
73+
dicts_equal(_b,c) && _suffix == suffix && _prefix == ""
74+
end
75+
76+
function test_convert(prefix::AbstractString,c,suffix::AbstractString;kwargs...)
77+
name = savename(prefix,c,suffix;kwargs...)
78+
_prefix, _b, _suffix = DrWatson.parse_savename(name)
79+
dicts_equal(_b,c) && _suffix == suffix && _prefix == prefix
80+
end
81+
82+
function test_convert(c;kwargs...)
83+
name = savename(c;kwargs...)
84+
_prefix, _b, _suffix = DrWatson.parse_savename(name)
85+
dicts_equal(_b,c) && _prefix == "" && _suffix == ""
86+
end
87+
88+
function dicts_equal(a,b)
89+
kA,kB = keys(a), keys(b)
90+
kA != kB && return false
91+
for k kA
92+
a[k] != b[k] && return false
93+
end
94+
return true
95+
end
96+
97+
98+
@test test_convert(
99+
Dict("c" => 0.1534, "u" => 5.1, "r"=>101, "mode" => "double"),
100+
digits=4)
101+
@test test_convert("prefix",
102+
Dict("c" => 0.1534, "u" => 5.1, "r"=>101, "mode" => "double"),
103+
digits=4)
104+
@test test_convert(
105+
Dict("c" => 0.1534, "u" => 5.1, "r"=>101, "mode" => "double"),
106+
"suffix",
107+
digits=4)
108+
@test test_convert("prefix",
109+
Dict("c" => 0.1534, "u" => 5.1, "r"=>101, "mode" => "double"),
110+
"suffix",
111+
digits=4)
112+
@test test_convert("prefix",
113+
Dict("c" => 0.1534, "u" => 5.1, "r"=>101, "variable_with_underscore" => "double"),
114+
"suffix",
115+
digits=4)
116+
@test test_convert(joinpath("a=10_mode=double","prefix"),
117+
Dict("c" => 0.1534, "u" => 5.1, "r"=>101, "variable_with_underscore" => "double"),
118+
"suffix",
119+
digits=4)
120+
@test test_convert(Dict("c" => 0.1534, "u" => 5.1),
121+
digits=4)
122+
@test test_convert(Dict("never" => "gonna", "give" => "you", "up" => "!"))
123+
@test test_convert(Dict("c" => 0.1534),
124+
digits=4)
125+
126+
b = Dict("c" => 0.1534, "u" => 5.1, "r"=>101, "mo_de" => "double")
127+
name = savename("prefix",b,connector="_",digits=4)
128+
_prefix, _b, _suffix = DrWatson.parse_savename(name,connector="_")
129+
@test dicts_equal(_b,b) && _prefix == "prefix" && _suffix == ""
130+
131+
_prefix, _b, _suffix = DrWatson.parse_savename(joinpath("some_random_path_a=10.0","prefix")*"_a=10_just_a_string=I'm not allowed to use underscores here_my_value=10.1.suffix_with_underscore-but-don't-use-dots")
132+
@test _prefix == joinpath("some_random_path_a=10.0","prefix")
133+
@test _suffix == "suffix_with_underscore-but-don't-use-dots"
134+
@test _b["a"] == 10.0
135+
@test _b["just_a_string"] == "I'm not allowed to use underscores here"
136+
@test _b["my_value"] == 10.1
137+
138+
@test_throws ErrorException DrWatson.parse_savename("a=10",connector="__")

0 commit comments

Comments
 (0)