/ Misc / src / helpers.jl
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