helpers.jl
1 # countdecimals(num::Float64) = abs(Base.Ryu.reduce_shortest(num)[2]) 2 # insert_and_dedup!(v::Vector, x) = (splice!(v, searchsorted(v,x), [x]); v) 3 4 @doc """Sets the offline mode. 5 6 $(TYPEDSIGNATURES) 7 8 This function sets the offline mode based on the `PLANAR_OFFLINE` environment variable. If the environment variable is set, it parses its value as a boolean to set the offline mode. 9 It is used to skip some errors during precompilation, if precompiling offline. 10 11 """ 12 setoffline!() = begin 13 opt = get(ENV, "PLANAR_OFFLINE", "") 14 OFFLINE[] = if opt == "" 15 false 16 else 17 @something tryparse(Bool, opt) false 18 end 19 end 20 21 isoffline() = OFFLINE[] 22 23 @doc """Same as the Lang.@ignore` macro, but only if `PLANAR_OFFLINE` is set.""" 24 macro skipoffline( 25 expr, 26 this_file=string(__source__.file), 27 this_line=__source__.line, 28 this_module=__module__, 29 ) 30 ex = if expr.head == :let 31 let_vars = expr.args[1] 32 # Main.e = let_vars 33 quote 34 let $(if let_vars.head == :(=) 35 (let_vars,) 36 elseif isempty(let_vars.args) 37 () 38 else 39 let_vars.args 40 end...) 41 $(@__MODULE__).@skipoffline $(expr.args[2]) $this_file $this_line $this_module 42 end 43 end 44 elseif expr.head == :block 45 this_expr = :( 46 begin end 47 ) 48 args = this_expr.args 49 n = 0 50 for line in expr.args 51 line isa LineNumberNode && continue 52 push!( 53 args, 54 :($(@__MODULE__).@skipoffline( 55 $line, $this_file, $(this_line + n), $this_module 56 )), 57 ) 58 n += 1 59 end 60 this_expr 61 else 62 quote 63 try 64 $expr 65 catch 66 if $(isoffline)() 67 @error "skipping error since offline" maxlog = 1 _module = $this_module _file = $this_file _line = $this_line exception = ( 68 first(Base.catch_stack())..., 69 ) 70 else 71 rethrow() 72 end 73 end 74 end 75 end 76 esc(ex) 77 end 78 79 @doc """Truncate a file to contain only the last `nlines` lines. 80 81 $(TYPEDSIGNATURES) 82 83 Opens the file for reading, keeps only the last `nlines` lines, then opens the file again for writing and overwrites it with those last lines. 84 Throws an error if `nlines` is not a positive integer. 85 """ 86 function truncate_file(filename, nlines) 87 # Check if nlines is positive 88 if nlines <= 0 89 error("nlines must be a positive integer") 90 end 91 f = open(filename, "a+") 92 lines = IOBuffer() 93 try 94 seekend(f) 95 current_pos = position(f) 96 count = 0 97 98 while count <= nlines && current_pos > 0 99 seek(f, current_pos - 1) 100 if Char(peek(f)) == '\n' 101 count += 1 102 seek(f, current_pos) 103 line = readline(f; keep=true) 104 write(lines, line) 105 seek(f, current_pos - 1) 106 end 107 current_pos -= 1 108 end 109 110 # Truncate the file at the current position 111 truncate(f, 0) 112 seekstart(lines) 113 seekstart(f) 114 write(f, lines) 115 finally 116 close(lines) 117 close(f) 118 end 119 end 120 121 @doc """Finds the module corresponding to a given symbol. 122 123 $(TYPEDSIGNATURES) 124 125 This function takes a symbol `sym` and attempts to find the corresponding module in the loaded modules. 126 127 """ 128 function _find_module(sym) 129 hasproperty(@__MODULE__, sym) && return getproperty(@__MODULE__, sym) 130 hasproperty(Main, sym) && return getproperty(Main, sym) 131 try 132 return @eval (using $sym; $sym) 133 catch 134 end 135 nothing 136 end 137 138 @doc """Creates a query from a struct type. 139 140 $(TYPEDSIGNATURES) 141 142 This function takes a struct type `T` and a separator `sep`, and creates a query string using the fields and their values in `T`. 143 144 """ 145 function queryfromstruct(T::Type, sep=","; kwargs...) 146 query = try 147 T(; kwargs...) 148 catch error 149 error isa ArgumentError && @error "Wrong query parameters for ($(st))." 150 rethrow(error) 151 end 152 params = Dict() 153 for s in fieldnames(T) 154 f = getproperty(query, s) 155 isnothing(f) && continue 156 ft = typeof(f) 157 hasmethod(length, (ft,)) && length(f) == 0 && continue 158 params[string(s)] = 159 ft != String && hasmethod(iterate, (ft,)) ? join(f, sep) : string(f) 160 end 161 params 162 end 163 164 @doc """Checks if a directory is empty. 165 166 $(TYPEDSIGNATURES) 167 168 This function takes a `path` and returns `true` if the directory at the given path is empty, and `false` otherwise. 169 170 """ 171 function isdirempty(path::AbstractString) 172 allpaths = collect(walkdir(path)) 173 length(allpaths) == 1 && isempty(allpaths[1][2]) && isempty(allpaths[1][3]) 174 end 175 176 _def(::Vector{<:AbstractFloat}) = NaN 177 _def(::Vector) = missing 178 @doc """Shifts elements in a vector. 179 180 $(TYPEDSIGNATURES) 181 182 This function shifts the elements in `arr` by `n` positions to the left. The new elements added to the end of the array are set to the value of `def`. 183 """ 184 function shift!(arr::Vector{<:AbstractFloat}, n=1, def=_def(arr)) 185 circshift!(arr, n) 186 if n >= 0 187 arr[begin:n] .= def 188 else 189 arr[(end + n):end] .= def 190 end 191 arr 192 end 193 194 @doc """Finds the range after a specified value in a vector. 195 196 $(TYPEDSIGNATURES) 197 198 This function takes a vector `v` and a value `d`, and returns a range that starts after the first occurrence of `d` in `v`. If `strict` is true, the range starts after `d`, otherwise it starts at `d`. 199 200 """ 201 function rangeafter(v::AbstractVector, d; strict=true, kwargs...) 202 r = searchsorted(v, d; kwargs...) 203 from = if length(r) > 0 204 ifelse(strict, r.start + length(r), r.start + 1) 205 else 206 r.start 207 end 208 from:lastindex(v) 209 end 210 211 @doc """Returns a view of the vector after a specified value. 212 213 $(TYPEDSIGNATURES) 214 215 This function returns a view of the vector `v` starting from the position after the first occurrence of `d`. The behavior can be adjusted using keyword arguments passed to `rangeafter`. 216 217 """ 218 after(v::AbstractVector, d; kwargs...) = view(v, rangeafter(v, d; kwargs...)) 219 220 @doc "Complement of [`rangeafter`](@ref)." 221 function rangebefore(v::AbstractVector, d; strict=true, kwargs...) 222 r = searchsorted(v, d; kwargs...) 223 to = if length(r) > 0 224 ifelse(strict, r.stop - length(r), r.stop - 1) 225 else 226 r.stop 227 end 228 firstindex(v):to 229 end 230 231 @doc "Complement of [`after`](@ref)." 232 before(v::AbstractVector, d; kwargs...) = view(v, rangebefore(v, d; kwargs...)) 233 234 @doc """Finds the range between two specified values in a vector. 235 236 $(TYPEDSIGNATURES) 237 238 This function takes a vector `v` and two values `left` and `right`, and returns a range that starts from the position of `left` and ends at the position of `right` in `v`. 239 240 """ 241 function rangebetween(v::AbstractVector, left, right; kwargs...) 242 l = rangeafter(v, left; kwargs...) 243 r = rangebefore(v, right; kwargs...) 244 (l.start):(r.stop) 245 end 246 247 @doc "Returns a view of the sorted vector `v`, indexed using `rangebetween`. 248 249 $(TYPEDSIGNATURES) 250 251 ```julia 252 julia> between([1, 2, 3, 3, 3], 3, 3; strict=true) 253 0-element view(::Vector{Int64}, 6:5) with eltype Int64 254 julia> between([1, 2, 3, 3, 3], 1, 3; strict=true) 255 1-element view(::Vector{Int64}, 2:2) with eltype Int64: 256 2 257 julia> between([1, 2, 3, 3, 3], 2, 3; strict=false) 258 2-element view(::Vector{Int64}, 3:4) with eltype Int64: 259 3 260 3 261 ``` 262 " 263 function between(v::AbstractVector, left, right; kwargs...) 264 view(v, rangebetween(v, left, right; kwargs...)) 265 end 266 267 @doc """Rewrites keys in a dictionary based on a function. 268 269 $(TYPEDSIGNATURES) 270 271 This function takes a dictionary `dict` and a function `f`, and rewrites each key in the dictionary by applying the function `f` to it. 272 273 """ 274 function rewritekeys!(dict::AbstractDict, f) 275 for (k, v) in dict 276 delete!(dict, k) 277 setindex!(dict, v, f(k)) 278 end 279 dict 280 end 281 282 @doc """Swaps keys in a dictionary based on a function and new key type. 283 284 $(TYPEDSIGNATURES) 285 286 This function takes a dictionary `dict`, a function `f`, and a new key type `k_type`. It returns a new dictionary of type `dict_type` where each key is transformed by the function `f` and cast to `k_type`. 287 288 """ 289 function swapkeys(dict::AbstractDict{K,V}, k_type::Type, f; dict_type=Dict) where {K,V} 290 out = dict_type{k_type,V}() 291 for (k, v) in dict 292 out[f(k)] = v 293 end 294 out 295 end 296 297 @doc """Checks if an iterable is strictly sorted. 298 299 $(TYPEDSIGNATURES) 300 301 This function takes an iterable `itr` and returns `true` if the elements in `itr` are strictly increasing, and `false` otherwise. 302 303 """ 304 function isstrictlysorted(itr...) 305 y = iterate(itr) 306 y === nothing && return true 307 prev, state = y 308 y = iterate(itr, state) 309 while y !== nothing 310 this, state = y 311 prev < this || return false 312 prev = this 313 y = iterate(itr, state) 314 end 315 return true 316 end 317 318 # slow version but precise 319 # roundfloat(val, prec) = begin 320 # inv_prec = 1.0 / prec 321 # round(round(val * inv_prec) / inv_prec, digits=abs(Base.Ryu.reduce_shortest(prec)[2])) 322 # end 323 324 roundfloat(val, prec) = begin 325 inv_prec = 1.0 / prec 326 round(val * inv_prec) / inv_prec 327 end 328 329 toprecision(n::Integer, prec::Integer) = roundfloat(n, prec) 330 @doc "When precision is a float it represents the pip. 331 332 $(TYPEDSIGNATURES) 333 " 334 function toprecision(n::T where {T<:Union{Integer,AbstractFloat}}, prec::AbstractFloat) 335 roundfloat(n, prec) 336 end 337 @doc "When precision is a Integer it represents the number of decimals. 338 339 $(TYPEDSIGNATURES) 340 " 341 function toprecision(n::AbstractFloat, prec::Int) 342 round(n; digits=prec) 343 end 344 345 @doc """ Round a float to a given precision (`SIGNIFICANT_DIGITS`). 346 347 $(TYPEDSIGNATURES) 348 349 `SIGNIFICANT_DIGITS` precision mode is similar to `DECIMAL_PLACES` except 350 that the last digit is the one that is rounded 351 352 """ 353 function toprecision(n::AbstractFloat, prec::UInt) 354 round(n; digits=prec - 1) 355 end 356 357 @doc """Checks if a value is approximately zero. 358 359 $(TYPEDSIGNATURES) 360 361 This function takes a value `v` and a tolerance `atol`. It returns `true` if the absolute difference between `v` and zero is less than or equal to `atol`, and `false` otherwise. 362 363 """ 364 approxzero(v::T; atol=ATOL) where {T} = isapprox(v, zero(T); atol) 365 @doc """Checks if a value is greater than or approximately equal to zero. 366 367 $(TYPEDSIGNATURES) 368 369 This function takes a value `v` and a tolerance `atol`. It returns `true` if `v` is greater than zero or if the absolute difference between `v` and zero is less than or equal to `atol`, and `false` otherwise. 370 371 """ 372 gtxzero(v::T; atol=ATOL) where {T} = v > zero(T) || isapprox(v, zero(T); atol) 373 ltxzero(v::T; atol=ATOL) where {T} = v < zero(T) || isapprox(v, zero(T); atol) 374 @doc "Alias to `abs`" 375 positive(v) = abs(v) 376 @doc "`negate(abs(v))`" 377 negative(v) = Base.negate(abs(v)) 378 @doc "Increment an integer reference by one" 379 inc!(v::Ref{I}) where {I<:Integer} = v[] += one(I) 380 @doc "Decrement an integer reference by one" 381 dec!(v::Ref{I}) where {I<:Integer} = v[] -= one(I) 382 @doc "Get the `attrs` field of the input object." 383 attrs(d) = getfield(d, :attrs) 384 @doc "Get all `keys...` from the `attrs` field of the input object. 385 386 $(TYPEDSIGNATURES) 387 " 388 attrs(d, keys...) = 389 let a = attrs(d) 390 (a[k] for k in keys) 391 end 392 @doc "Get `k` from the `attrs` field of the input object. 393 394 $(TYPEDSIGNATURES) 395 " 396 attr(d, k) = attrs(d)[k] 397 @doc "Get `k` from the `attrs` field of the input object, or `v` if `k` is not present. 398 399 $(TYPEDSIGNATURES) 400 " 401 attr(d, k, v) = get(attrs(d), k, v) 402 @doc "Get `k` from the `attrs` field of the input object, or `v` if `k` is not present, setting `k` to `v`. 403 404 $(TYPEDSIGNATURES) 405 " 406 attr!(d, k, v) = get!(attrs(d), k, v) 407 @doc "Set `k` in the `attrs` field of the input object to `v`. 408 409 $(TYPEDSIGNATURES) 410 " 411 modifyattr!(d, v, op, keys...) = 412 let a = attrs(d) 413 for k in keys 414 a[k] = op(a[k], v) 415 end 416 end 417 @doc "Set `k` in the `attrs` field of the input object to `v`." 418 setattr!(d, v, keys...) = setindex!(attrs(d), v, keys...) 419 @doc "Check if `k` is present in the `attrs` field of the input object." 420 hasattr(d, k) = haskey(attrs(d), k) 421 @doc "Check if any of `keys...` is present in the `attrs` field of the input object." 422 hasattr(d, keys...) = 423 let a = attrs(d) 424 any(haskey(a, k) for k in keys) 425 end 426 427 @doc """ 428 Returns an iterator that yields unique elements from an iterable. 429 430 $(TYPEDSIGNATURES) 431 """ 432 struct UniqueIterator{T, I} 433 iter::I 434 seen::Set{T} 435 by::Function 436 end 437 438 UniqueIterator(iter; by=identity) = UniqueIterator(iter, Set{eltype(iter)}(), by) 439 440 function Base.iterate(it::UniqueIterator, state=iterate(it.iter)) 441 local byitem 442 state === nothing && return nothing 443 item, next_state = state 444 byitem = it.by(item) 445 446 while byitem in it.seen 447 state = iterate(it.iter, next_state) 448 state === nothing && return nothing 449 item, next_state = state 450 byitem = it.by(item) 451 end 452 453 push!(it.seen, byitem) 454 return (item, iterate(it.iter, next_state)) 455 end 456 Base.IteratorSize(::Type{<:UniqueIterator}) = Base.SizeUnknown() 457 Base.eltype(::Type{UniqueIterator{T,I}}) where {T,I} = T 458 459 export shift! 460 export approxzero, gtxzero, ltxzero, negative, positive, inc!, dec! 461 export attrs, attr, hasattr, attr!, setattr!, modifyattr! 462 export isoffline, @skipoffline, UniqueIterator 463