diff --git a/Manifest.toml b/Manifest.toml index 3ed2817..25bd670 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,12 @@ julia_version = "1.10.0" manifest_format = "2.0" -project_hash = "9288745b0c141040f1c8126efff08e5e2d46709d" +project_hash = "0e345527edcf441c2de8a2d1c00b2b6466582291" + +[[deps.AbstractTrees]] +git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" +uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +version = "0.4.5" [[deps.ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" @@ -185,9 +190,9 @@ uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" version = "1.10.0" [[deps.TestHandcalcFunctions]] -git-tree-sha1 = "3df775c88fa815d10b9ca754e276256fa78cb41d" +git-tree-sha1 = "1e4ebeb8d7d78f9974c0153321e80fa66487d3bf" uuid = "6ba57fb7-81df-4b24-8e8e-a3885b6fcae7" -version = "0.1.0" +version = "0.2.0" [[deps.UUIDs]] deps = ["Random", "SHA"] diff --git a/Project.toml b/Project.toml index f2d68a5..010f402 100644 --- a/Project.toml +++ b/Project.toml @@ -1,9 +1,10 @@ name = "Handcalcs" uuid = "e8a07092-c156-4455-ab8e-ed8bc81edefb" authors = ["Cole Miller"] -version = "0.2.1" +version = "0.3.0" [deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" @@ -18,7 +19,7 @@ LaTeXStrings = "1.3" Latexify = "0.16" MacroTools = "0.5" Revise = "3.5" -TestHandcalcFunctions = "0.1" +TestHandcalcFunctions = "0.2" Unitful = "1.19" UnitfulLatexify = "1.6" julia = "1.10" diff --git a/docs/src/index.md b/docs/src/index.md index bdb7e22..8e2f5d2 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -10,7 +10,7 @@ This is the documentation for [Handcalcs.jl](https://github.com/co1emi11er2/Hand **Note: This package now renders properly in Quarto/Weave!! You can change the default settings to your liking. See examples below.** -## Examples +## Expression Examples ### A single expression example: @@ -65,19 +65,60 @@ a, b, c = 1, 2, 3 z = 6 end cols=3 spa=0 ``` +## Function Examples + +The `@handcalcs` macro will now automatically try to "unroll" the expressions within a function when the expression has the following pattern: `variable = function_name(args...; kwargs...)`. Note that this is recursive, so if you have a function that calls other functions where the expressions that call the function are of the format mentioned, it will continue to step into each function to "unroll" all expressions. + +One issue that can arise are for the functions that you do not want to unroll. Consider the expression: `y = sin(x)` or ` y = x + 5`. Both of these expressions match the format: `variable = function_name(args...; kwargs...)` and would be unrolled. This would result in an error since these functions don't have generic math expressions that can be latexified defining the function. You will need to use the `not_funcs` keyword to manually tell the @handcalcs macro to pass over these functions. Some of the common math functions that you will not want to unroll are automatically passed over. See examples below. ### An example for rendering expressions within a function: +```@example other +function calc_Ix(b, h) # function defined in TestHandcalcFunctions + Ix = b*h^3/12 + return Ix +end; +``` + ```@example main using TestHandcalcFunctions b = 5 # width h = 15 # height -@handfunc Ix = calc_Ix(b, h) # function is defined in TestHandcalcFunctions package +@handcalcs Ix = calc_Ix(b, h) # function is defined in TestHandcalcFunctions package ``` -The `Ix` variable is evaluated. Ix being the variable assigned in the @handfunc part (variables within function are not defined in the global name space). If you assign it to a different variable then that will be the variable defined (although you will still see it as Ix in the latex portion). Also note that return statements are filtered out of the function body, so keep relevant parts separate from return statements. +The `Ix` variable is evaluated. Ix being the variable assigned in the @handcalcs part (variables within function are not defined in the global name space). If you assign it to a different variable then that will be the variable defined (although you will still see it as Ix in the latex portion). Also note that return statements are filtered out of the function body, so keep relevant parts separate from return statements. + +```@example other +function calc_Is(b, h) # function defined in TestHandcalcFunctions + Ix = calc_Ix(b, h) + Iy = calc_Iy(h, b) + return Ix, Iy +end; +``` + +```@example main +using TestHandcalcFunctions +x = 0 +@handcalcs begin +y = sin(x) +z = cos(x) +I_x, I_y = TestHandcalcFunctions.calc_Is(5, 15) +end not_funcs = [sin cos] +``` + +In the above example `sin` and `cos` were passed over and calc_Is was not. As you can see, the calc_Is function was a function that called other functions, and the @handcalcs macro continued to step into each function to unroll all expressions. Please see below for a list of the current functions that are passed over automatically. Please submit a pull request if you would like to add more generic math functions that I have left out. + +``` +const math_syms = [ + :*, :/, :^, :+, :-, :%, + :.*, :./, :.^, :.+, :.-, :.%, + :<, :>, Symbol(==), :<=, :>=, + :.<, :.>, :.==, :.<=, :.>=, + :sqrt, :sin, :cos, :tan] +``` -Current Limitations for `@handfunc` +Current Limitations for `@handcalcs` - I believe the function needs to be defined in another package. The @code_expr macro from CodeTracking.jl does not see functions in Main for some reason. - If the function has other function calls within it's body that are not available in Main, then the macro will error. diff --git a/src/Handcalcs.jl b/src/Handcalcs.jl index 66e36f3..eb6a783 100644 --- a/src/Handcalcs.jl +++ b/src/Handcalcs.jl @@ -9,8 +9,9 @@ using MacroTools using LaTeXStrings using CodeTracking, Revise using InteractiveUtils +import AbstractTrees: Leaves -export @handcalc, @handcalcs, @handfunc, multiline_latex +export @handcalc, @handcalcs, @handfunc, multiline_latex, collect_exprs export set_handcalcs, reset_handcalcs, get_handcalcs #, initialize_format export latexify, @latexdefine, set_default, get_default, reset_default @@ -20,7 +21,12 @@ export latexify, @latexdefine, set_default, get_default, reset_default # set_default(fmt=x->format(round(x, digits=4))) # end # end -const math_syms = [:*, :/, :^, :+, :-, :%, :.*, :./, :.^, :.+, :.-, :.%, :sqrt, :sin, :cos, :tan] +const math_syms = [ + :*, :/, :^, :+, :-, :%, + :.*, :./, :.^, :.+, :.-, :.%, + :<, :>, Symbol(==), :<=, :>=, + :.<, :.>, :.==, :.<=, :.>=, + :sqrt, :sin, :cos, :tan] const h_syms = [:cols, :spa, :h_env] include("default_h_kwargs.jl") diff --git a/src/handcalc_marco.jl b/src/handcalc_marco.jl index 77ad630..5bc49bb 100644 --- a/src/handcalc_marco.jl +++ b/src/handcalc_marco.jl @@ -2,7 +2,8 @@ """ @handcalc expression -Create `LaTeXString` representing `expression`. The expression being a vaiable followed by an equals sign and an algebraic equation. +Create `LaTeXString` representing `expression`. The expression being a vaiable followed by +an equals sign and an algebraic equation. Any side effects of the expression, like assignments, are evaluated as well. The RHS can be formatted or otherwise transformed by supplying a function as kwarg `post`. @@ -21,14 +22,14 @@ julia> c """ macro handcalc(expr, kwargs...) expr = unblock(expr) - expr = rmlines(expr) - # if @capture(expr, x_ = f_(fields__) | f_(fields__)) #future recursion - # if f ∉ math_syms - # return esc(:((@macroexpand @handfunc $expr))) - # end - # # return :($esc(@handfunc $expr)) - # end - + expr = rmlines(expr) + if @capture(expr, x_ = f_(fields__) | f_(fields__)) # Check if function call + if f ∉ math_syms && check_not_funcs(f, kwargs) + kwargs = kwargs..., :(is_recursive = true) + return esc(:(@handfunc $(expr) $(kwargs...))) + end + end + expr_post = expr.head == :(=) ? expr.args[2:end] : expr expr_numeric = _walk_expr(expr_post, math_syms) params = _extractparam.(kwargs) @@ -53,12 +54,12 @@ function _handcalc(expr, expr_numeric, post, kwargs) Expr( :call, :latexify, - Expr(:parameters, _extractparam.(kwargs)...), - Expr(:call, :Expr, - QuoteNode(:(=)), Meta.quot(expr), # symbolic portion - Expr(:call, :Expr, - QuoteNode(:(=)), Meta.quot(expr_numeric), # numeric portion - Expr(:call, post, _executable(expr)))), # defines variable + Expr(:parameters, _extractparam.(kwargs)...), + Expr(:call, :Expr, + QuoteNode(:(=)), Meta.quot(expr), # symbolic portion + Expr(:call, :Expr, + QuoteNode(:(=)), Meta.quot(expr_numeric), # numeric portion + Expr(:call, post, _executable(expr)))), # defines variable ), ) end @@ -77,7 +78,7 @@ function _executable(expr) end _extractparam(arg::Symbol) = arg -_extractparam(arg::Expr) = Expr(:kw, arg.args[1], arg.args[2]) +_extractparam(arg::Expr) = Expr(:kw, arg.args[1], arg.args[2]) # *************************************************** @@ -88,7 +89,7 @@ _extractparam(arg::Expr) = Expr(:kw, arg.args[1], arg.args[2]) # *************************************************** function numeric_sub(x) - Expr(:($), x) + Expr(:($), x) end function _walk_expr(expr::Vector, math_syms) @@ -110,7 +111,7 @@ function _walk_expr(expr::Vector, math_syms) count = 1 return numeric_sub(ex) end - return ex + return ex end end @@ -133,7 +134,7 @@ function _walk_expr(expr::Expr, math_syms) count = 1 return numeric_sub(ex) end - return ex + return ex end end @@ -147,4 +148,49 @@ function _det_branch_size(expr; count=3) # determines field arg depth end # *************************************************** -# *************************************************** \ No newline at end of file +# *************************************************** + +function check_not_funcs(f, kwargs) + not_funcs = find_not_funcs(kwargs) + not_funcs = typeof(not_funcs) == Symbol ? [not_funcs] : not_funcs + return f ∉ not_funcs +end + +function find_not_funcs(kwargs) + not_funcs = [] + for kwarg in kwargs + split_kwarg = _split_kwarg(kwarg) + if split_kwarg == :not_funcs + not_funcs = parse_not_funcs(kwarg.args[2]) + end + end + return not_funcs +end + +function parse_not_funcs(value::QuoteNode) + [value.value] +end + +function parse_not_funcs(value::Symbol) + [value] +end + +function parse_not_funcs(expr::Expr) + not_funcs = [] + if expr.head in [:vcat :hcat :vect] + for arg in expr.args + push!(not_funcs, parse_not_func(arg)) + end + elseif expr.head == :. + return [expr] + end + return not_funcs +end + +function parse_not_func(value::QuoteNode) + value.value +end + +function parse_not_func(value) + value +end diff --git a/src/handcalcs_macro.jl b/src/handcalcs_macro.jl index 9bf5bc1..f6c735c 100644 --- a/src/handcalcs_macro.jl +++ b/src/handcalcs_macro.jl @@ -2,9 +2,9 @@ """ @handcalcs expressions -Create `LaTeXString` representing `expressions`. The expressions representing a number of expressions. -A single expression being a vaiable followed by an equals sign and an algebraic equation. -Any side effects of the expression, like assignments, are evaluated as well. +Create `LaTeXString` representing `expressions`. The expressions representing a number of +expressions. A single expression being a vaiable followed by an equals sign and an algebraic +equation. Any side effects of the expression, like assignments, are evaluated as well. The RHS can be formatted or otherwise transformed by supplying a function as kwarg `post`. Can also add comments to the end of equations. See example below. @@ -43,20 +43,19 @@ julia> d macro handcalcs(expr, kwargs...) expr = unblock(expr) expr = rmlines(expr) - h_kwargs, kwargs = clean_kwargs(kwargs) # parse handcalc kwargs (h_kwargs) - + is_recursive, h_kwargs, kwargs = clean_kwargs(kwargs) # parse handcalc kwargs (h_kwargs) exprs = [] #initialize expression accumulator # If singular symbol if typeof(expr) == Symbol push!(exprs, :(@latexdefine $(expr) $(kwargs...))) - return _handcalcs(exprs, h_kwargs) + return is_recursive ? _handcalcs_recursive(exprs) : _handcalcs(exprs, h_kwargs) end # If singular expression if expr.head == :(=) push!(exprs, :(@handcalc $(expr) $(kwargs...))) - return _handcalcs(exprs, h_kwargs) + return is_recursive ? _handcalcs_recursive(exprs) : _handcalcs(exprs, h_kwargs) end # If multiple Expressions @@ -72,7 +71,7 @@ macro handcalcs(expr, kwargs...) error("Code pieces should be of type string or expression") end end - return _handcalcs(exprs, h_kwargs) + return is_recursive ? _handcalcs_recursive(exprs) : _handcalcs(exprs, h_kwargs) end function _handcalcs(exprs, h_kwargs) @@ -81,6 +80,20 @@ function _handcalcs(exprs, h_kwargs) Expr(:parameters, _extractparam.(h_kwargs)...), exprs...))) end +function _handcalcs_recursive(exprs) + Expr(:block, esc( + Expr(:call, :collect_exprs, + exprs...))) +end + +function collect_exprs(exprs...) # just collect when exprs recursive + exprs_array = [] + for expr in exprs + push!(exprs_array, expr) + end + return exprs_array +end + function multiline_latex(exprs...; kwargs...) h_kwargs = merge(default_h_kwargs, kwargs) return process_multiline_latex(exprs...;h_kwargs...) @@ -93,23 +106,36 @@ function clean_expr(expr) expr = replace(expr, "="=>"&=", count=1) # add alignment end -function clean_kwargs(kwargs) +# Splits handcalc kwargs and latexify kwargs +# Also checks for recursion (if @handcalcs is being called from @handcalc) +function clean_kwargs(kwargs) + is_recursive = false h_kwargs = [] l_kwargs = [] for kwarg in kwargs - if _split_kwarg(kwarg) in h_syms + split_kwarg = _split_kwarg(kwarg) + if split_kwarg in h_syms h_kwargs = push!(h_kwargs, kwarg) + elseif split_kwarg == :is_recursive + is_recursive = true else l_kwargs = push!(l_kwargs, kwarg) end end - return Tuple(h_kwargs), Tuple(l_kwargs) + return is_recursive, Tuple(h_kwargs), Tuple(l_kwargs) end _split_kwarg(arg::Symbol) = arg _split_kwarg(arg::Expr) = arg.args[1] -function process_multiline_latex(exprs...;cols=1, spa=10, h_env="aligned", kwargs...) +function process_multiline_latex( + exprs...; + cols=1, + spa=10, + h_env="aligned", + kwargs... + ) + exprs = collect(Leaves(exprs)) # This handles nested vectors when recursive cols_start = cols multi_latex = "\\begin{$h_env}" for (i, expr) in enumerate(exprs) diff --git a/src/handfunc_macro.jl b/src/handfunc_macro.jl index e4a9596..a40a568 100644 --- a/src/handfunc_macro.jl +++ b/src/handfunc_macro.jl @@ -1,7 +1,8 @@ """ @handfunc expression -Create `LaTeXString` representing `expressions`. These expressions represent a number of expressions that exist within the function that was called. +Create `LaTeXString` representing `expressions`. These expressions represent a number of +expressions that exist within the function that was called. A single expression being a variable followed by an equals sign and the function being called. The expression is evaluated as well (not the expressions within the function). The RHS can be formatted or otherwise transformed by supplying a function as kwarg `post`. diff --git a/test/handcalcs_macro.jl b/test/handcalcs_macro.jl index 4ef7974..7955296 100644 --- a/test/handcalcs_macro.jl +++ b/test/handcalcs_macro.jl @@ -15,7 +15,7 @@ calc = @handcalcs x = (-b + sqrt(b^2 - 4*a*c))/(2*a) calc2 = @handcalcs begin x = (-b + sqrt(b^2 - 4*a*c))/(2*a) end @test calc == expected @test x == 2.0 -@test calc2 == expected +@test calc2 == expected # *************************************************** @@ -99,7 +99,7 @@ y = [4; 5; 6] calc = @handcalcs z = x + y calc2 = @handcalcs z = x .* y -@test calc == replace(expected, "\r" => "") # for whatever reason the expected had addittional carriage returns (\r) +@test calc == replace(expected, "\r" => "") # for whatever reason the expected had addittional carriage returns (\r) @test calc2 == replace(expected2, "\r" => "") # *************************************************** @@ -158,3 +158,49 @@ calc = @handcalcs begin a end @test calc == expected # *************************************************** + +# not_funcs test +# *************************************************** +# *************************************************** +expected = L"$\begin{aligned} +I_{x} &= \mathrm{calc}_{Ix}\left( 5, 15 \right) = 1406.25 +\end{aligned}$" + +calc1 = @handcalcs begin + I_x = calc_Ix(5,15) +end not_funcs = calc_Ix + +calc2 = @handcalcs begin + I_x = calc_Ix(5,15) +end not_funcs = :calc_Ix + +@test calc1 == expected +@test calc2 == expected + +expected = L"$\begin{aligned} +Ix &= \mathrm{calc}_{Ix}\left( b, h \right) = \mathrm{calc}_{Ix}\left( 5, 15 \right) = 1406.25 +\\[10pt] +Iy &= \mathrm{calc}_{Iy}\left( h, b \right) = \mathrm{calc}_{Iy}\left( 15, 5 \right) = 156.25 +\end{aligned}$" + +calc1 = @handcalcs begin + I_s = calc_Is(5,15) +end not_funcs = [calc_Ix calc_Iy] + +calc2 = @handcalcs begin + I_s = calc_Is(5,15) +end not_funcs = [:calc_Ix :calc_Iy] + +calc3 = @handcalcs begin + I_s = calc_Is(5,15) +end not_funcs = [calc_Ix, calc_Iy] + +calc4 = @handcalcs begin + I_s = calc_Is(5,15) +end not_funcs = [calc_Ix; calc_Iy] + +@test calc1 == expected +@test calc2 == expected +@test calc3 == expected +@test calc4 == expected +# *************************************************** diff --git a/test/handfunc_macro.jl b/test/handfunc_macro.jl index 13177de..8e15dbf 100644 --- a/test/handfunc_macro.jl +++ b/test/handfunc_macro.jl @@ -93,4 +93,16 @@ calc = @handfunc area = TestHandcalcFunctions.area_rectangle(rec.b, rec.h) @test calc == expected # *************************************************** -# Check recursion and other functions within function body \ No newline at end of file +# Check recursion and other functions within function body +# *************************************************** +# *************************************************** +expected = L"$\begin{aligned} +Ix &= \frac{b \cdot h^{3}}{12} = \frac{5 \cdot 15^{3}}{12} = 1406.25 +\\[10pt] +Iy &= \frac{h \cdot b^{expo}}{denominator} = \frac{15 \cdot 5^{3}}{12} = 156.25 +\end{aligned}$" +b = 5 +h = 15 +calc = @handfunc Is = TestHandcalcFunctions.calc_Is(b, h) +@test calc == expected +# *************************************************** \ No newline at end of file