text:
module LibModbus

using LibModbus_jll

export ModbusMapping, TcpContext, RtuContext
export MbExcpt, MODBUS_ERROR_RECOVERY_NONE, MODBUS_ERROR_RECOVERY_LINK
export MODBUS_ERROR_RECOVERY_PROTOCOL
export modbus_context_valid
export modbus_set_slave, modbus_get_slave, modbus_set_socket!, modbus_get_socket
export modbus_get_response_timeout, modbus_set_response_timeout, modbus_get_byte_timeout
export modbus_set_byte_timeout, modbus_set_error_recovery
export modbus_get_header_length, modbus_connect
export modbus_close, modbus_flush, modbus_free!, modbus_set_debug
export modbus_read_bits, modbus_read_input_bits, modbus_read_registers
export modbus_read_input_registers, modbus_write_bit, modbus_write_register
export modbus_write_bits, modbus_write_registers, modbus_mask_write_register
export modbus_write_and_read_registers, modbus_send_raw_request
export modbus_reply_exception, modbus_report_slave_id
export modbus_mapping_new_start_address, modbus_mapping_new
export modbus_mapping_free!, modbus_receive, modbus_reply
export modbus_tcp_listen, modbus_tcp_accept, tcp_close
export modbus_rtu_set_serial_mode, modbus_rtu_get_serial_mode
export modbus_rtu_set_rts, modbus_get_rts, modbus_set_rts_delay
export modbus_get_rts_delay

@enum Modbus_error_recovery_mode::Cint begin
    MODBUS_ERROR_RECOVERY_NONE = 0
    MODBUS_ERROR_RECOVERY_LINK = 1<<1
    MODBUS_ERROR_RECOVERY_PROTOCOL = 1<<2
end

@enum MbExcpt::UInt8 begin
    MODBUS_EXCEPTION_ILLEGAL_FUNCTION = 0x01
    MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS
    MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE
    MODBUS_EXCEPTION_SLAVE_OR_SERVER_FAILURE
    MODBUS_EXCEPTION_ACKNOWLEDGE
    MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY
    MODBUS_EXCEPTION_NEGATIVE_ACKNOWLEDGE
    MODBUS_EXCEPTION_MEMORY_PARITY
    MODBUS_EXCEPTION_NOT_DEFINED
    MODBUS_EXCEPTION_GATEWAY_PATH
    MODBUS_EXCEPTION_GATEWAY_TARGET
    MODBUS_EXCEPTION_MAX
end

mutable struct ModbusMapping
    nb_bits::Cint
    start_bits::Cint
    nb_input_bits::Cint
    start_input_bits::Cint
    nb_input_registers::Cint
    start_input_registers::Cint
    nb_registers::Cint
    start_registers::Cint
    tab_bits::Ptr{UInt8}
    tab_input_bits::Ptr{UInt8}
    tab_input_registers::Ptr{UInt16}
    tab_registers::Ptr{UInt16}
end

abstract type ModbusContext end
mutable struct Modbus_t end

struct TcpContext <: ModbusContext
    ip_address::String
    port::Int
    _ctx_ptr::Ref{Ptr{Modbus_t}}
    function TcpContext(ip_address::String, port::Integer)
        ctx_ptr = ccall((:modbus_new_tcp, libmodbus), Ptr{Modbus_t}, (Cstring, Cint),
                        ip_address, port)
        if ctx_ptr == C_NULL
            _strerror(-1, "TcpContext()")
        end
        ctx = new(ip_address, port, ctx_ptr)
    end
end
const MODBUS_TCP_MAX_ADU_LENGTH = 260

modbus_context_valid(ctx::ModbusContext) = ctx._ctx_ptr[] != C_NULL

function Base.show(io::IO, ctx::TcpContext)
    color = modbus_context_valid(ctx) ? :green : :red
    str = modbus_context_valid(ctx) ? "ip $(ctx.ip_address), port $(ctx.port)" : "NULL"
    printstyled(io, "TcpContext($(str))"; color)
end

struct RtuContext <: ModbusContext
    device::String
    baud::Integer
    parity::Symbol
    data_bit::Integer
    stop_bit::Integer
    _ctx_ptr::Ref{Ptr{Modbus_t}}
    function RtuContext(
        device::String,
        baud::Integer,
        parity::Symbol,
        data_bit::Integer,
        stop_bit::Integer
        )
        if parity === :none
            prt = 'N'
        elseif parity === :even
            prt = 'E'
        elseif parity === :odd
            prt = 'O'
        else
            error("unknown parity value specified")
        end
        ctx_ptr = ccall((:modbus_new_rtu, libmodbus), Ptr{Modbus_t},
                        (Cstring, Cint, Cchar, Cint, Cint),
                        device, baud, prt, data_bit, stop_bit)
        if ctx_ptr == C_NULL
            _strerror(-1, "RtuContext()")
        end
        ctx = new(device, baud, parity, data_bit, stop_bit, ctx_ptr)
        return ctx
    end
end

function Base.show(io::IO, ctx::RtuContext)
    color = modbus_context_valid(ctx) ? :green : :red
    str =  modbus_context_valid(ctx) ? "RtuContext(device $(ctx.device), baud $(ctx.baud), "*
        "parity $(ctx.parity)), data_bit $(ctx.data_bit), stop_bit $(ctx.stop_bit))" : "NULL"
    printstyled(io, str; color)
end

# Common for RTU and TCP contexts
"""
    modbus_set_slave(ctx::ModbusContext, slave::Integer) -> Int32

Set the slave number in the libmodbus context. The behavior depends of network
and the role of the device:

RTU

Define the slave ID of the remote device to talk in master mode or set the internal
slave ID in slave mode. According to the protocol, a Modbus device must only accept
message holding its slave number or the special broadcast number.

TCP

The slave number is only required in TCP if the message must reach a device
on a serial network.

The broadcast address is MODBUS_BROADCAST_ADDRESS. This special value must be used
when you want all Modbus devices of the network receive the request.

# Arguments
- `ctx::ModbusContext`: libmodbus context
-`slave::Integer`: slave number

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function modbus_set_slave(ctx::ModbusContext, slave::Integer)
    ret = ccall((:modbus_set_slave, libmodbus), Cint, (Ptr{Cvoid}, Cint),
                ctx._ctx_ptr[], slave)
    _strerror(ret, "modbus_set_slave()")

    return ret
end

"""
    modbus_get_slave(ctx::ModbusContext) -> Int32

Get the slave number in the libmodbus context.

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Int`: slave number if successful, otherwise return -1 and set errno
"""
function modbus_get_slave(ctx::ModbusContext)
    ret = ccall((:modbus_get_slave, libmodbus), Cint, (Ptr{Cvoid},), ctx._ctx_ptr[])
    _strerror(ret, "modbus_get_slave()")

    return ret
end

"""
    modbus_set_socket!(ctx::ModbusContext, s::Integer) -> Int32

Set the socket or file descriptor in the libmodbus context. This function is useful
for managing multiple client connections to the same server.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `s::Integer`: socket or file descriptor

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function modbus_set_socket!(ctx::ModbusContext, s::Integer)
    ret = ccall((:modbus_set_socket, libmodbus), Cint, (Ptr{Cvoid}, Cint), ctx._ctx_ptr[], s)
    _strerror(ret, "modbus_set_socket!()")

    return ret
end

"""
    modbus_get_socket(ctx::ModbusContext) -> Int32

Get the current socket of the context

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Int`: current socket or file descriptor of the context if successful,
    otherwise return -1 and set errno
"""
function modbus_get_socket(ctx::ModbusContext)
    ret = ccall((:modbus_get_socket, libmodbus), Cint, (Ptr{Cvoid},), ctx._ctx_ptr[])
    _strerror(ret, "modbus_get_socket()")

    return ret
end

"""
    modbus_get_response_timeout(ctx::ModbusContext) -> Int32,Int,Int

Get timeout interval used to wait for a response

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
- `to_sec::Int`: seconds of the response timeout
- `to_usec`: microseconds of the response timeout
"""
function modbus_get_response_timeout(ctx::ModbusContext)
    to_sec = Ref{UInt32}()
    to_usec = Ref{UInt32}()
    ret = ccall((:modbus_get_response_timeout, libmodbus), Cint,
                (Ptr{Cvoid}, Ref{UInt32}, Ref{UInt32}),
                ctx._ctx_ptr[], to_sec, to_usec)
    _strerror(ret, "modbus_get_response_timeout()")

    return ret,Int(to_sec[]),Int(to_usec[])
end

"""
    modbus_set_response_timeout(ctx::ModbusContext, to_sec::Integer, to_usec::Integer)
                                                                                -> Int32

Set the timeout interval used to wait for a response. When a byte timeout is set,
if elapsed time for the first byte of response is longer than the given timeout,
an ETIMEDOUT error will be raised by the function waiting for a response.
When byte timeout is disabled, the full confirmation response must be received before
expiration of the response timeout.

The value of to_usec argument must be in the range 0 to 999999.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `to_sec::Integer`: seconds of the response timeout
- `to_usec::Integer`: microseconds part of the response timeout

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function modbus_set_response_timeout(ctx::ModbusContext,  to_sec::Integer, to_usec::Integer)
    ret = ccall((:modbus_set_response_timeout, libmodbus), Cint,
                (Ptr{Cvoid}, UInt32, UInt32), ctx._ctx_ptr[], to_sec, to_usec)
    _strerror(ret, "modbus_set_response_timeout()")

    return ret
end

"""
    modbus_get_byte_timeout(ctx::ModbusContext) -> Int32,Int,Int

Timeout interval between two consecutive bytes of the same message

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
- `to_sec::Int`: seconds of the byte timeout
- `to_usec`: microseconds of the byte timeout
"""
function modbus_get_byte_timeout(ctx::ModbusContext)
    to_sec = Ref{UInt32}(0)
    to_usec = Ref{UInt32}(0)
    ret = ccall((:modbus_get_byte_timeout, libmodbus), Cint,
                (Ptr{Cvoid}, Ref{UInt32}, Ref{UInt32}), ctx._ctx_ptr[], to_sec, to_usec)
    _strerror(ret, "modbus_get_byte_timeout()")

    return ret,Int(to_sec[]),Int(to_usec[])
end

"""
    modbus_set_byte_timeout(ctx::ModbusContext, to_sec::Integer, to_usec::Integer) -> Int32

Set the timeout interval between two consecutive bytes of the same message. The timeout
is an upper bound on the amount of time elapsed before select() returns, if
the time elapsed is longer than the defined timeout, an ETIMEDOUT error will be raised
by the function waiting for a response.

The value of to_usec argument must be in the range 0 to 999999.

If both to_sec and to_usec are zero, this timeout will not be used at all.
In this case, modbus_set_response_timeout() governs the entire handling of the response,
the full confirmation response must be received before expiration of the response timeout.
When a byte timeout is set, the response timeout is only used to wait for
until the first byte of the response.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `to_sec::Int`: seconds of the byte timeout
- `to_usec`: microseconds of the byte timeout

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function modbus_set_byte_timeout(ctx::ModbusContext, to_sec::Integer, to_usec::Integer)
    ret = ccall((:modbus_set_byte_timeout, libmodbus), Cint,
                (Ptr{Cvoid}, UInt32, UInt32), ctx._ctx_ptr[], to_sec, to_usec)
    _strerror(ret, "modbus_set_byte_timeout()")

    return ret
end

# function modbus_get_indication_timeout(ctx::ModbusContext, uint32_t *to_sec, uint32_t *to_usec)

#   # int
# end

# function modbus_set_indication_timeout(ctx::ModbusContext, uint32_t to_sec, uint32_t to_usec)

#   # int
# end
"""
    modbus_set_error_recovery(ctx::ModbusContext, err_rec::Integer) -> Int32

Set the error recovery mode to apply when the connection fails or the byte received is not
expected. The argument error_recovery may be bitwise-or’ed with zero or more
of the following constants.

By default there is no error recovery (MODBUS_ERROR_RECOVERY_NONE) so the application
is responsible for controlling the error values returned by libmodbus functions
and for handling them if necessary.

When MODBUS_ERROR_RECOVERY_LINK is set, the library will attempt an reconnection
after a delay defined by response timeout of the libmodbus context. This mode will try
an infinite close/connect loop until success on send call and will just try one time
to re-establish the connection on select/read calls (if the connection was down,
the values to read are certainly not available any more after reconnection,
except for slave/server). This mode will also run flush requests after a delay
based on the current response timeout in some situations (eg. timeout of select call).
The reconnection attempt can hang for several seconds if the network to the remote target
unit is down.

When MODBUS_ERROR_RECOVERY_PROTOCOL is set, a sleep and flush sequence will be used
to clean up the ongoing communication, this can occurs when the message length is invalid,
the TID is wrong or the received function code is not the expected one.
The response timeout delay will be used to sleep.

The modes are mask values and so they are complementary.

It’s not recommended to enable error recovery for slave/server.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `to_sec::Int`: seconds of the byte timeout
- `to_usec`: microseconds of the byte timeout

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function  modbus_set_error_recovery(ctx::ModbusContext, err_rec::Integer)
    ret = ccall((:modbus_set_error_recovery, libmodbus), Cint,
                (Ptr{Cvoid}, Cint), ctx._ctx_ptr[], Cint(err_rec))
    _strerror(ret, "modbus_set_error_recovery()")

    return ret
end

"""
    modbus_get_header_length(ctx::ModbusContext) -> Int32

Retrieve the current header length from the backend.

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Int`: the header length as an integer value, -1 in case of error.
"""
function modbus_get_header_length(ctx::ModbusContext)
    ret = ccall((:modbus_get_header_length, libmodbus), Cint, (Ptr{Cvoid},), ctx._ctx_ptr[])
    _strerror(ret, "modbus_get_header_length()")

    return ret
end

"""
    modbus_connect(ctx::ModbusContext) -> Int32

Establish a connection to a Modbus server, a network or a bus using the context information
of libmodbus context given in argument.

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function modbus_connect(ctx::ModbusContext)
    ret = ccall((:modbus_connect, libmodbus), Cint, (Ptr{Cvoid},), ctx._ctx_ptr[])
    _strerror(ret, "modbus_connect()")

    return ret
end

"""
    modbus_close(ctx::ModbusContext)

Close the connection established with the backend set in the Modbus context.

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
nothing
"""
function modbus_close(ctx::ModbusContext)
    ccall((:modbus_close, libmodbus), Cvoid, (Ptr{Cvoid},), ctx._ctx_ptr[])
end

"""
    modbus_free!(ctx::ModbusContext)

Free an allocated ModbusContext structure.

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
nothing
"""
function modbus_free!(ctx::ModbusContext)
    ccall((:modbus_free, libmodbus), Cvoid, (Ptr{Cvoid},), ctx._ctx_ptr[])
    ctx._ctx_ptr[] = C_NULL

    return nothing
end

"""
    modbus_flush(ctx::ModbusContext) -> Int32

Discard data received but not read to the socket or file descriptor associated
to the context.

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 or the number of flushed bytes
    if successful.
"""
function modbus_flush(ctx::ModbusContext)
    ret = ccall((:modbus_flush, libmodbus), Cint, (Ptr{Cvoid},), ctx._ctx_ptr[])
    _strerror(ret, "modbus_flush()")

    return ret
end

"""
    modbus_set_debug(ctx::ModbusContext, flag::Bool) -> Int32

Set the debug flag of the context. By default, the boolean flag is set to false.
When the flag value is set to true, display verbose messages.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `flag::Bool`: display debug information if true

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function modbus_set_debug(ctx::ModbusContext, flag::Bool)
    ret = ccall((:modbus_set_debug, libmodbus), Cint, (Ptr{Cvoid},Cint),
                ctx._ctx_ptr[], flag)
    _strerror(ret, "modbus_set_debug()")

    return ret
end

function _strerror(return_code::Integer, message::AbstractString)
    err_no = Libc.errno()
    if return_code < 0
        return_code = err_no
        str = ccall((:modbus_strerror,libmodbus), Cstring, (Cint,), err_no)
        @warn "$(message): "*unsafe_string(str)
    end

    return nothing
end

"""
    modbus_read_bits(ctx::ModbusContext, addr::Integer, nb::Integer) -> Int32,BitVector

Read the status of the nb bits (coils) at the address addr of the remote device and
return a bit vector of the results.

The function uses the Modbus function code 0x01 (read coil status).

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `addr::Integer`: address to begin reading at
- `nb::Integer`: number of coils to read

# Returns
- `rc::Int`: the return code is -1 in case of error, number of read bits if successful.
- `bv::BitVector`: bitarray of the coils' state
"""
function modbus_read_bits(ctx::ModbusContext, addr::Integer, nb::Integer)
    dest = Vector{UInt8}(undef, nb)
    ret = ccall((:modbus_read_bits, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, Cint, Ref{UInt8}),
                ctx._ctx_ptr[], addr, nb, dest)
    _strerror(ret, "modbus_read_bits()")
    bv = ret > 0 ? BitVector(dest[1:ret]) : BitVector[]

    return ret,bv
end

"""
    modbus_read_input_bits(ctx::ModbusContext, addr::Integer, nb::Integer) -> Int32,BitVector

Read the nb input bits beginning at the address addr of the remote device and
return a bit vector of the results.

The function uses the Modbus function code 0x02 (read input status).

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `addr::Integer`: address to begin reading at
- `nb::Integer`: number of coils to read

# Returns
- `rc::Int`: the return code is -1 in case of error, number of read bits if successful.
- `bv::BitVector`: bitarray of the coils' state.
"""
function modbus_read_input_bits(ctx::ModbusContext, addr::Integer, nb::Integer)
    dest = Vector{UInt8}(undef, nb)
    ret = ccall((:modbus_read_input_bits, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, Cint, Ref{UInt8}),
                ctx._ctx_ptr[], addr, nb, dest)
    _strerror(ret, "modbus_read_input_bits()")
    bv = ret > 0 ? BitVector(dest[1:ret]) : BitVector[]

    return ret,bv
end

"""
    modbus_read_registers(ctx::ModbusContext, addr::Integer, nb::Integer) -> Int32,Vector

Read the content of the nb holding registers beginning at the address addr of
the remote device. The result of reading is stored in dest array as word values (16 bits).

The function uses the Modbus function code 0x03 (read holding registers).

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `addr::Integer`: address to begin reading at
- `nb::Integer`: number of registers to read

# Returns
- `rc::Int`: the return code is -1 in case of error, number of read registers if successful.
- `v::Vector{UInt16}`: contents of the holding registers
"""
function modbus_read_registers(ctx::ModbusContext, addr::Integer, nb::Integer)
    dest = Vector{UInt16}(undef, nb)
    ret = ccall((:modbus_read_registers, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, Cint, Ref{UInt16}), ctx._ctx_ptr[], addr, nb, dest)
    _strerror(ret, "modbus_read_registers()")
    ret <= 0 || ret == nb || @warn "read $(ret) registers instead of $(nb)"

    return ret,dest[1:ret]
end

"""
    modbus_read_input_registers(ctx::ModbusContext, addr::Integer, nb::Integer) -> Int32

Read the content of the nb input registers to address addr of the remote device.
The result of the reading is stored in dest array as word values (16 bits).

The function uses the Modbus function code 0x04 (read input registers). The holding
registers and input registers have different historical meaning, but nowadays
it’s more common to use holding registers only.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `addr::Integer`: address to begin reading at
- `nb::Integer`: number of registers to read

# Returns
- `rc::Int`: the return code is -1 in case of error, number of read registers if successful.
- `v::Vector{UInt16}`: contents of the input registers
"""
function modbus_read_input_registers(ctx::ModbusContext, addr::Integer, nb::Integer)
    dest = Vector{UInt16}(undef, nb)
    ret = ccall((:modbus_read_input_registers, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, Cint, Ref{UInt16}), ctx._ctx_ptr[], addr, nb, dest)
    _strerror(ret, "modbus_read_input_registers()")
    ret <= 0 || ret == nb || @warn "read $(ret) registers instead of $(nb)"

    return ret,dest[1:ret]
end

"""
    modbus_write_bit(ctx::ModbusContext, coil_addr::Integer, status::Bool) -> Int32

Wwrite the status at the address addr of the remote device.

The function uses the Modbus function code 0x05 (force single coil).

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `coil_addr::Integer`: address of the coil
- `status`: status to write

# Returns
- `rc::Int`: the return code is -1 in case of error, 1 if successful.
"""
function modbus_write_bit(ctx::ModbusContext, coil_addr::Integer, status)
    ret = ccall((:modbus_write_bit, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, Cint), ctx._ctx_ptr[], coil_addr, status)
    _strerror(ret, "modbus_write_bit()")

    return ret
end

"""
    modbus_write_register(ctx::ModbusContext, reg_addr::Integer, value::Integer) -> Int32

Write the value to the  holding register at the address addr of the remote device.

The function uses the Modbus function code 0x06 (preset single register).

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `addr::Integer`: address of the holding register
- `value::Integer`: value to write to the register

# Returns
- `rc::Int`: the return code is -1 in case of error, 1 if successful.
"""
function modbus_write_register(ctx::ModbusContext, reg_addr::Integer, value::Integer)
    ret = ccall((:modbus_write_register, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, UInt16), ctx._ctx_ptr[], reg_addr, value)
    _strerror(ret, "modbus_write_register()")

    return ret
end

"""
     modbus_write_bits(ctx::ModbusContext, addr::Integer, data::Vector) -> Int32

Write the status of the nb bits (coils) of data beginning at the address addr
of the remote device. The src array must contain bytes or logical values.

The function uses the Modbus function code 0x0F (force multiple coils).

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `addr::Integer`: address of beginning coil
- `data::Integer`: values to write to the coils

# Returns
- `rc::Int`: the return code is -1 in case of error, number of written bits if successful.
"""
function modbus_write_bits(
    ctx::ModbusContext, addr::Integer, data::Vector{T}
    ) where T<:Union{Bool,UInt8}
    nb = length(data)
    ret = ccall((:modbus_write_bits, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, Cint, Ref{UInt8}), ctx._ctx_ptr[], addr, nb, data)
    _strerror(ret, "modbus_write_bits()")
    ret <= 0 || ret == nb || @warn "wrote $(ret) bits instead of $(nb)"

    return ret
end

"""
    modbus_write_registers(ctx::ModbusContext, addr::Integer, data::Vector) -> Int32

Write the content of array data[] beginning at the address addr of the remote device.

The function uses the Modbus function code 0x10 (preset multiple registers).

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `addr::Integer`: address of the first register to write
- `data::Integer`: values to write to the registers

# Returns
- `rc::Int`: the return is -1 in case of error, number of written registers if successful.
"""
function modbus_write_registers(
    ctx::ModbusContext, addr::Integer, data::AbstractVector{UInt16}
    )
    nb = length(data)
    ret = ccall((:modbus_write_registers, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, Cint, Ref{UInt16}), ctx._ctx_ptr[], addr, nb, data)
    _strerror(ret, "modbus_write_registers()")
    ret <= 0 || ret == nb || @warn "wrote $(ret) registers instead of $(nb)"

    return ret
end
"""
    modbus_mask_write_register(args...) -> Int32

 Modify the value of the holding register at the address addr of the remote device
using the algorithm:

new value = (current value AND and_mask) OR (or_mask AND (NOT 'and'))

The function uses the Modbus function code 0x16 (mask single register).

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `addr::Integer`: address of the register to modify
- `and_mask::UInt16`: AND mask
- `or_mask::UInt16`: OR mask

# Returns
- `rc::Int`: the return is -1 in case of error, 1 if successful.
"""
function modbus_mask_write_register(ctx::ModbusContext,
                                    addr::Integer,
                                    and_mask::UInt16,
                                    or_mask::UInt16)
    ret = ccall((:modbus_mask_write_register, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, UInt16, UInt16),
                ctx._ctx_ptr[], addr, and_mask, or_mask)
    _strerror(ret, "modbus_mask_write_register()")

    return ret
end

"""
    modbus_write_and_read_registers(args...) -> Int32,Vector

Write the content of the array write_data[] to the holding registers beginning
at the adddress write_addr, then read the content of the read_nb holding registers
beginning at the address read_addr.

The function uses the Modbus function code 0x17 (write/read registers).

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `write_addr::Integer`: address of the holding register to begin writing at
- `write_data::AbstractVector{UInt16}`: data to write to the device
- `read_addr::Integer`: address to begin reading holding registers from
- `read_nb::UInt16`: number of holding registers to read

# Returns
- `rc::Int`: the return code is -1 in case of error, number of read registers if successful.
"""
function modbus_write_and_read_registers(
    ctx::ModbusContext,
    write_addr::Integer,
    write_data::AbstractVector{UInt16},
    read_addr::Integer,
    read_nb::Integer
    )
    write_nb = length(write_data)
    dest = Vector{UInt16}(undef, read_nb)
    ret = ccall((:modbus_write_and_read_registers, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, Cint, Ref{UInt16}, Cint, Cint, Ref{UInt16}),
                ctx._ctx_ptr[], write_addr, write_nb, write_data, read_addr,
                read_nb, dest)
    _strerror(ret, "modbus_write_and_read_registers()")
    ret <= 0 || ret == read_nb || @warn "read $(ret) registers instead of $(read_nb)"

    return ret,dest[1:ret]
end

"""
     modbus_send_raw_request(ctx::ModbusContext, raw_req::AbstractVector{UInt8}) -> Int32

Send a request via the socket of the context ctx. This function must be used
for debugging purposes because you have to take care to make a valid request by hand.
The function only adds to the message the header or CRC of the selected backend,
so raw_req must start and contain at least a slave/unit identifier and a function code.
This function can be used to send request not handled by the library.

The public header of libmodbus provides a list of supported Modbus functions codes,
prefixed by MODBUS_FC_ (eg. MODBUS_FC_READ_HOLDING_REGISTERS), to help build of raw requests.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `raw_req::Vector{UInt8}`: byte sequence to send

# Returns
- `rc::Int`: the return code is -1 in case of error, the full message length,
     counting the extra data relating to the backend, if successful.
"""
function modbus_send_raw_request(ctx::ModbusContext, raw_req::AbstractVector{UInt8})
    nb = length(raw_req)
    ret = ccall((:modbus_send_raw_request, libmodbus), Cint,
                (Ptr{Cvoid}, Ref{UInt8}, Cint),
                ctx._ctx_ptr[], raw_req, nb)
    _strerror(ret, "modbus_send_raw_request()")

    return ret
end

"""
    modbus_reply_exception(args...) -> Int32

Send an exception reponsebased on the exception_code in argument.

The libmodbus provides the following exception codes:
    MODBUS_EXCEPTION_ILLEGAL_FUNCTION (1)
    MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS (2)
    MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE (3)
    MODBUS_EXCEPTION_SLAVE_OR_SERVER_FAILURE (4)
    MODBUS_EXCEPTION_ACKNOWLEDGE (5)
    MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY (6)
    MODBUS_EXCEPTION_NEGATIVE_ACKNOWLEDGE (7)
    MODBUS_EXCEPTION_MEMORY_PARITY (8)
    MODBUS_EXCEPTION_NOT_DEFINED (9)
    MODBUS_EXCEPTION_GATEWAY_PATH (10)
    MODBUS_EXCEPTION_GATEWAY_TARGET (11)
The initial request req is required to build a valid response.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `req::Vector{UInt8}`: request to respond to
- ` exception_code::MbExcpt`: exception code to send

# Returns
- `rc::Int`: the return code is -1 in case of error, length of the response sent if
    successful.
"""
function modbus_reply_exception(
    ctx::ModbusContext, req::AbstractVector{UInt8}, exception_code::MbExcpt
    )
    ret = ccall((:modbus_reply_exception, libmodbus), Cint,
                (Ptr{Cvoid}, Ref{UInt8}, Cuint),
                ctx._ctx_ptr[], req, exception_code)

    _strerror(ret, "modbus_reply_exception()")

    return ret
end

"""
    modbus_report_slave_id(ctx::ModbusContext, max_dest::Integer) -> Int32,Vector

Send a request to the controller to obtain a description of the controller.
The response stored in dest contains:
- the slave ID, this unique ID is in reality not unique at all so it’s not possible to depend on it to know how the information are packed in the response.
- the run indicator status (0x00 = OFF, 0xFF = ON)
- additional data specific to each controller. For example, libmodbus returns the version of the library as a string.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `max_dest::Integer`: number of bytes sent from the device to return to caller

# Returns
- `rc::Int`: the return code is -1 in case of error, number of read data bytes
    if successful. If the output was truncated due to the max_dest limit then
    the return value is the number of bytes which would have been written to dest
    if enough space had been available. Thus, a return value greater than max_dest
    means that the response data was truncated.
- `vec::Vector{UInt8}`: response from the controller
"""
function modbus_report_slave_id(ctx::ModbusContext, max_dest::Integer)
    dest = Vector{UInt8}(undef, max_dest)
    ret = ccall((:modbus_report_slave_id, libmodbus), Cint,
                (Ptr{Cvoid}, Cint, Ref{UInt8}), ctx._ctx_ptr[], max_dest, dest)
    _strerror(ret, "modbus_report_slave_id()")
    ret <= max_dest || @warn "$(ret) bytes of output truncated to $(max_dest)"

    return ret,dest[1:min(max_dest, ret)]
end

"""
    modbus_mapping_new_start_address(args...) -> Ptr{ModbusMapping}

Allocate four arrays to store bits, input bits, registers and inputs registers.
The pointers are stored in modbus_mapping_t structure. All values of the arrays
are initialized to zero.

The different starting adresses make it possible to place the mapping at any address
in each address space. This way, you can give access to values stored at high adresses
without allocating memory from the address zero, for eg. to make available registers
from 10000 to 10009, you can use:

   mb_mapping = modbus_mapping_new_start_address(0, 0, 0, 0, 10000, 10, 0, 0);

With this code, only 10 registers (uint16_t) are allocated.

If it isn’t necessary to allocate an array for a specific type of data, you can pass the zero value in argument, the associated pointer will be NULL.

This function may be used to handle requests in a Modbus server/slave.

# Arguments
- `start_bits::Integer`: beginning address for coils
- `nb_bits::Integer`: number of coils
- `start_input_bits::Integer`: beginning address for input bit registers
- `nb_input_bits::Integer`: number of input bit registers
- `start_registers::Integer`: beginning address for holding registers
- `nb_registers::Integer`: number of holding registers
- `start_input_registers::Integer`: beginning address for input registers
- `nb_input_registers::Integer`: number of input registers

# Returns pointer to ModbusMapping structure if successful, C_NULL pointer otherwise
"""
function modbus_mapping_new_start_address(
    start_bits::Integer,
    nb_bits::Integer,
    start_input_bits::Integer,
    nb_input_bits::Integer,
    start_registers::Integer,
    nb_registers::Integer,
    start_input_registers::Integer,
    nb_input_registers::Integer
    )
    mbm_ptr = ccall((:modbus_mapping_new_start_address, libmodbus), Ptr{ModbusMapping},
                    (Cint, Cint, Cint, Cint, Cint, Cint, Cint, Cint),
                    start_bits, nb_bits, start_input_bits, nb_input_bits,
                    start_registers, nb_registers, start_input_registers,nb_input_registers)
    _strerror(-Int(mbm_ptr == C_NULL), "modbus_mapping_new_start_address()")

    return mbm_ptr
end

"""
     modbus_mapping_new_start_address(args...) -> Ptr{ModbusMapping}

Allocate four arrays to store bits, input bits, registers and inputs registers.
The pointers are stored in modbus_mapping_t structure. All values of the arrays
are initialized to zero.

This function is equivalent to a call of the modbus_mapping_new_start_address()
function with all start addresses to 0.

If it isn’t necessary to allocate an array for a specific type of data, you can pass
the zero value in argument, the associated pointer will be NULL.

# Arguments
- `nb_bits::Integer`: number of coils
- `nb_input_bits::Integer`: number of input bit registers
- `nb_registers::Integer`: number of holding registers
- `nb_input_registers::Integer`: number of input registers

# Returns
-`ptr`: pointer to ModbusMapping structure if successful, C_NULL pointer otherwise
"""
function modbus_mapping_new(
    nb_bits::Integer,
    nb_input_bits::Integer,
    nb_registers::Integer,
    nb_input_registers::Integer
    )
    mbm_ptr = ccall((:modbus_mapping_new, libmodbus), Ptr{ModbusMapping},
                    (Cint, Cint, Cint, Cint),
                    nb_bits, nb_input_bits, nb_registers, nb_input_registers)
    _strerror(-Int(mpm_ptr == C_NULL), "modbus_mapping_new()")

    return mbm_ptr
end

"""
    modbus_mapping_free!(mbm_ptr::Ptr{ModbusMapping})

Free the four arrays of ModbusMappping structure pointed to by mbm_ptr and then
the structure itself.

"""
function modbus_mapping_free!(mbm_ptr::Ptr{ModbusMapping})
    ccall((:modbus_mapping_free, libmodbus), Cvoid,
          (Ptr{ModbusMapping},), mbm_ptr)
end

"""
    modbus_receive(ctx::TcpContext) -> rc,Vector

Receive an indication request from the socket of the context ctx. This function is used
by Modbus slave/server to receive and analyze indication request sent by the masters/clients.

If you need to use another socket or file descriptor then the one defined in the context ctx,
see the function modbus_set_socket().

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Int`: the return code is -1 in case of error, the request length if successful.
- `req::Vector{UInt8}`: the indication request received.
"""
function modbus_receive(ctx::TcpContext)
    req = Vector{UInt8}(undef, MODBUS_TCP_MAX_ADU_LENGTH)
    ret = ccall((:modbus_receive, libmodbus), Cint, (Ptr{Cvoid}, Ref{UInt8}),
                ctx._ctx_ptr[], req)
    _strerror(ret, "modbus_receive()")

    return ret,req[1:ret]
end

"""
    modbus_reply(args...) -> Int32

Send a response to received request. The request req given in argument is analyzed,
a response is then built and sent by using the information of the modbus context ctx.

If the request indicates to read or write a value, the operation will done in the modbus
mapping mb_mapping according to the type of the manipulated data.

If an error occurs, an exception response will be sent.

This function is designed for Modbus server.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `req::AbstractVector{UInt8}`: request received by server
- `mbm_ptr::Ptr{ModbusMapping}`: pointer to the structure describing the registers

# Returns
- `rc::Int`: the return code is -1 in case of error, length of the response sent
     if successful.
"""
function modbus_reply(
    ctx::ModbusContext, req::AbstractVector{UInt8}, mbm_ptr::Ptr{ModbusMapping}
    )
    req_length = length(req)
    ret = ccall((:modbus_reply, libmodbus), Cint,
                (Ptr{Cvoid}, Ref{UInt8}, Cint, Ptr{Cvoid}),
                ctx._ctx_ptr[], req, req_length, mbm_ptr)
    _strerror(ret, "modbus_reply()")

    return ret
end

"""
    modbus_tcp_listen(ctx::TcpContext, nb_connection::Integer) -> Int32

Create a socket and listen to maximum nb_connection incoming connections on the specified IP
address. The context ctx must be created and initialized by the constructor TcpContext().
If IP address is set to NULL or 0.0.0.0, any addresses will be listened to.

# Arguments
- `ctx::ModbusContext`: libmodbus TCP context
- `nb_connection::Integer`: maximum number of incoming connections.

# Returns
- `rc::Int`: the return code is -1 in case of error, a new socket if successful.
"""
function modbus_tcp_listen(ctx::TcpContext, nb_connection::Integer)
    ret = ccall((:modbus_tcp_listen, libmodbus), Cint, (Ptr{Cvoid}, Cint),
                ctx._ctx_ptr[], nb_connection)
    _strerror(ret, "modbus_tcp_listen()")

    return ret
end

"""
    modbus_tcp_accept(ctx::TcpContext, s::Int32) -> Int32

Extract the first connection on the queue of pending connections, create a new socket
and store it in the libmodbus context.

See unit_test_server() function in tests for an example.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `s::Int32`: socket to listen on

# Returns
- `rc::Int`: the return code is -1 in case of error, new socket
"""
function modbus_tcp_accept(ctx::TcpContext, s::Cint)
    ret = ccall((:modbus_tcp_accept, libmodbus), Cint, (Ptr{Cvoid}, Ref{Cint}),
                ctx._ctx_ptr[], s)
    _strerror(ret, "modbus_tcp_accept()")

    return ret
end

"""
    tcp_close(sockfd::Integer) -> Int32

Close Libc descriptor.

"""
function tcp_close(sockfd::Integer)
    ret = ccall(:close, Cint,(Cint,), sockfd)
    _strerror(ret, "tcp_close()")

    return ret
end

# RTU context
const MODBUS_RTU_RS232 = 0
const MODBUS_RTU_RS485 = 1
const MODBUS_RTU_RTS_NONE =  0
const MODBUS_RTU_RTS_UP = 1
const MODBUS_RTU_RTS_DOWN = 2

"""
    modbus_rtu_set_serial_mode(ctx::RtuContext, mode::Symbol) -> Int32

Set the selected serial mode:

MODBUS_RTU_RS232

- the serial line is set for RS232 communication. RS-232 (Recommended Standard 232)
is the traditional name for a series of standards for serial binary single-ended data
and control signals connecting between a DTE (Data Terminal Equipment) and
a DCE (Data Circuit-terminating Equipment). It is commonly used in computer serial ports

MODBUS_RTU_RS485

- the serial line is set for RS485 communication. EIA-485, also known as TIA/EIA-485
or RS-485, is a standard defining the electrical characteristics of drivers and receivers
for use in balanced digital multipoint systems. This standard is widely used
for communications in industrial automation because it enables commication over
long distances and in electrically noisy environments.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `mode::Symbol`: one of the symbols :RS485 or :RS232

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function modbus_rtu_set_serial_mode(ctx::RtuContext, mode::Symbol)
    if mode is :RS485
        m_code = MODBUS_RTU_RS485
    elseif mode is :RS232
        m_code = MODBUS_RTU_RS232
    else
        error("unknown serial mode")
    end
    ret = ccall((:modbus_rtu_set_serial_mode, libmodbus), Cint,
                (Ptr{Cvoid}, Cint), ctx._ctx_ptr[], m_code)
    _strerror(ret, "modbus_rtu_set_serial_mode()")

    return ret
end

"""
    modbus_rtu_get_serial_mode(ctx::RtuContext) -> Int32

Return the serial mode currently used by the libmodbus context as one of the symbols
:RS232 or :RS485

# Arguments
- `ctx::ModbusContext`: libmodbus context
ctx::RtuContext

# Returns
- `rc::Symbol`: the return code is :Nothing in case of error, seriall mode if successful.
"""
function modbus_rtu_get_serial_mode(ctx::RtuContext)
    ret = ccall((:modbus_rtu_get_serial_mode, libmodbus), Cint,
                (Ptr{Cvoid},), ctx._ctx_ptr[])
    _strerror(ret, "modbus_rtu_get_serial_mode()")
    if ret == MODBUS_RTU_RS485
        mode = :RS485
    elseif ret == MODBUS_RTU_RS232
        mode = :RS232
    else
        @warn "unknown mode code returned by modbus_get_serial_mode()"
        mode = :Nothing
    end

    return mode
end

"""
    modbus_rtu_set_rts(ctx::RtuContext, mode::Symbol) -> Int32

Set the Request To Send mode to communicate on a RS485 serial bus. By default, the mode
is set to :RTS_NONE and no signal is issued before writing data on the wire.

To enable the RTS mode, the symbols :RTS_UP or :RTS_DOWN must be used, these modes
enable the RTS mode and set the polarity at the same time. When :RTS_UP is used,
an ioctl call is made with RTS flag enabled then data is written on the bus
after a delay of 1 ms, then another ioctl call is made with the RTS flag disabled
and again a delay of 1 ms occurs. The :RTS_DOWN mode applies the same procedure
but with an inverted RTS flag.

Can only be used with a context created by RtuContext() constructor.

# Arguments
- `ctx::ModbusContext`: libmodbus context created by the RtsContext() constructor
- `mode::Symbol`: Request To Send mode

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function modbus_rtu_set_rts(ctx::RtuContext, mode::Symbol)
    if mode is :RTS_NONE
        m_code = MODBUS_RTU_RTS_NONE
    elseif mode is :RTS_UP
        m_code = MODBUS_RTU_RTS_UP
    elseif mode is :RTS_DOWN
        m_code = MODBUS_RTU_RTS_DOWN
    else
        error("unknown rts mode")
    end
    ret = ccall((:modbus_rtu_set_rts_mode, libmodbus), Cint,
                (Ptr{Cvoid}, Cint), ctx._ctx_ptr[], m_code)
    _strerror(ret, "modbus_rtu_set_rts_mode()")

    return ret
end

"""
    modbus_rtu_get_rts(ctx::RtuContext) -> Symbol

Get the current Request To Send mode of the libmodbus context ctx. The possible returned
values are: :RTS_NONE, :RTS_UP, :RTS_DOWN and :Nothing

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Symbol`: the return is :Nothing in case of error, 0 if successful.
"""
function modbus_rtu_get_rts(ctx::RtuContext)
    ret = ccall((:modbus_rtu_get_rts, libmodbus), Cint,
                (Ptr{Cvoid},), ctx._ctx_ptr[])
    _strerror(ret, "modbus_rtu_get_rts()")
    if ret == MODBUS_RTU_RTS_NONE
        mode = :RTS_NONE
    elseif ret == MODBUS_RTU_RTS_UP
        mode = :RTS_UP
    elseif ret == MODBUS_RTU_RTS_DOWN
        mode = :RTS_DOWN
    else
        @warn "unknown RTS code returned by modbus_get_rts()"
        mode = :Nothing
    end

    return mode
end

"""
    modbus_rtu_set_rts_delay(ctx::RtuContext, us::Int) -> Int32

Set the Request To Send delay period of the libmodbus context ctx.
This function can only be used with a context created by RtuContext() contructor.

# Arguments
- `ctx::ModbusContext`: libmodbus context
- `us::Int`:  Request To Send delay period in microseconds

# Returns
- `rc::Int`: the return code is -1 in case of error, 0 if successful.
"""
function modbus_rtu_set_rts_delay(ctx::RtuContext, us::Int)
    ret = ccall((:modbus_rtu_set_rts_delay, libmodbus), Cint,
                (Ptr{Cvoid}, Cint), ctx._ctx_ptr[], us)
    _strerror(ret, "modbus_rtu_set_rts_delay()")

    return ret
end

"""
    modbus_rtu_get_rts_delay(ctx::RtuContext) -> Int32

Get the current Request To Send delay period of the libmodbus context ctx.
This function can only be used with a context created by RtuContext() contructor.

# Arguments
- `ctx::ModbusContext`: libmodbus context

# Returns
- `rc::Int`: the return code is -1 in case of error, the current RTS delay in microseconds
if successful.
"""
function modbus_rtu_get_rts_delay(ctx::RtuContext)
    ret = ccall((:modbus_rtu_get_rts_delay, libmodbus), Cint,
                (Ptr{Cvoid},), ctx._ctx_ptr[])
    _strerror(ret, "modbus_rtu_get_rts_delay()")

    return ret
end

end


text:
const text_samples = [
   "Various math formulas, primes and common Julia operators" =>
   """
   ẋ, x⃗, x⃗̇, x͍⃡, x↑, θ, θ̂

   ∀x ∈ ℝ⁺, ∃n ∈ ℕ : n ≤ x < n+1

   𝐹(s) = ℒ{f(t)} = ∫₀᪲ ℯ⁻ˢᵗ f(t) dt

   J⃗ = ∇⃗ × H⃗, B⃗ = μ₀(M⃗ + H⃗)

   ∂ₜu - Δu*(γ₁-α)*α̂ = 0

   ¬(a ∨ b) ≡ (¬a ∧ ¬b), (a → b) ⊨ (¬b → ¬a)

   ' " ′ ″ ‴ ⁗ ‵ ‶ ‷
   => -> |> ∘ ⨟ ⋅ × ⊗ + ⊕ ≈ ∝ ~

   Mᵢ,ⱼ,ₖ  Mᵢ‚ⱼ‚ₖ  Mᵢˏⱼˏₖ  Mᵢ⸒ⱼ⸒ₖ  Mᵢˌⱼˌₖ

   ⌠  x⁴ - 5x       ⌠ ⌠ ⌠
   ⎮ ───────── dx   ⎮⃘ ⎮⃙ ⎮⃚
   ⌡  ∛(x-1)        ⌡ ⌡ ⌡
   """

   "Julia code for a Gibbs sampler" =>
   """
   # From https://discourse.julialang.org/t/50416/31

   using Distributions, Statistics

   function mc(x, iterations, μ₀ = 0., τ₀ = 1e-7, α = 0.0001, β = 0.0001)
       ∑x, n = sum(x), length(x)
       μ, τ = ∑x/n, 1/var(x)
       μₛ = rand(Normal((τ₀*μ₀ + τ*∑x)/(τ₀ + n*τ), 1/√(τ₀ + n*τ)), iterations)
       τₛ = rand(Gamma(α + n/2, 1/(β + 0.5*sum((xᵢ-μ)^2 for xᵢ in x))), iterations)
       samples = [(μ, τ); collect(zip(μₛ, τₛ))]
   end
   """

   "Julia code making heavy use of Unicode characters" =>
   """
   # From https://github.com/zygmuntszpak/MultipleViewGeometry.jl

   function cost(c::CostFunction, entity::FundamentalMatrix, 𝛉::AbstractArray,
                 𝒞::Tuple{AbstractArray, Vararg{AbstractArray}},
                 𝒟::Tuple{AbstractArray, Vararg{AbstractArray}})
       ℳ, ℳʹ = 𝒟
       Λ₁, Λ₂ = 𝒞
       Jₐₘₗ = 0.0
       N = length(𝒟[1])
       𝚲ₙ = @MMatrix zeros(4,4)
       𝐞₁ = @SVector [1.0, 0.0, 0.0]
       𝐞₂ = @SVector [0.0, 1.0, 0.0]
       index = SVector(1,2)
       @inbounds for n = 1:N
           𝚲ₙ[1:2,1:2] .=  Λ₁[n][index,index]
           𝚲ₙ[3:4,3:4] .=  Λ₂[n][index,index]
           𝐦 = hom(ℳ[n])
           𝐦ʹ= hom(ℳʹ[n])
           𝐔ₙ = (𝐦 ⊗ 𝐦ʹ)
           ∂ₓ𝐮ₙ =  [(𝐞₁ ⊗ 𝐦ʹ) (𝐞₂ ⊗ 𝐦ʹ) (𝐦 ⊗ 𝐞₁) (𝐦 ⊗ 𝐞₂)]
           𝐁ₙ =  ∂ₓ𝐮ₙ * 𝚲ₙ * ∂ₓ𝐮ₙ'
           𝚺ₙ = 𝛉' * 𝐁ₙ * 𝛉
           𝚺ₙ⁻¹ = inv(𝚺ₙ)
           Jₐₘₗ +=  𝛉' * 𝐔ₙ * 𝚺ₙ⁻¹ * 𝐔ₙ' * 𝛉
       end
       Jₐₘₗ
   end
   """

   "Lowercase letters" =>
   raw"""
   regular  a b c d e f g h i j k l m n o p q r s t u v w x y z
   \sans    𝖺 𝖻 𝖼 𝖽 𝖾 𝖿 𝗀 𝗁 𝗂 𝗃 𝗄 𝗅 𝗆 𝗇 𝗈 𝗉 𝗊 𝗋 𝗌 𝗍 𝗎 𝗏 𝗐 𝗑 𝗒 𝗓
   \tt      𝚊 𝚋 𝚌 𝚍 𝚎 𝚏 𝚐 𝚑 𝚒 𝚓 𝚔 𝚕 𝚖 𝚗 𝚘 𝚙 𝚚 𝚛 𝚜 𝚝 𝚞 𝚟 𝚠 𝚡 𝚢 𝚣
   \it      𝑎 𝑏 𝑐 𝑑 𝑒 𝑓 𝑔 ℎ 𝑖 𝑗 𝑘 𝑙 𝑚 𝑛 𝑜 𝑝 𝑞 𝑟 𝑠 𝑡 𝑢 𝑣 𝑤 𝑥 𝑦 𝑧
   \isans   𝘢 𝘣 𝘤 𝘥 𝘦 𝘧 𝘨 𝘩 𝘪 𝘫 𝘬 𝘭 𝘮 𝘯 𝘰 𝘱 𝘲 𝘳 𝘴 𝘵 𝘶 𝘷 𝘸 𝘹 𝘺 𝘻
   \scr     𝒶 𝒷 𝒸 𝒹 ℯ 𝒻 ℊ 𝒽 𝒾 𝒿 𝓀 𝓁 𝓂 𝓃 ℴ 𝓅 𝓆 𝓇 𝓈 𝓉 𝓊 𝓋 𝓌 𝓍 𝓎 𝓏
   \bf      𝐚 𝐛 𝐜 𝐝 𝐞 𝐟 𝐠 𝐡 𝐢 𝐣 𝐤 𝐥 𝐦 𝐧 𝐨 𝐩 𝐪 𝐫 𝐬 𝐭 𝐮 𝐯 𝐰 𝐱 𝐲 𝐳
   \bsans   𝗮 𝗯 𝗰 𝗱 𝗲 𝗳 𝗴 𝗵 𝗶 𝗷 𝗸 𝗹 𝗺 𝗻 𝗼 𝗽 𝗾 𝗿 𝘀 𝘁 𝘂 𝘃 𝘄 𝘅 𝘆 𝘇
   \bi      𝒂 𝒃 𝒄 𝒅 𝒆 𝒇 𝒈 𝒉 𝒊 𝒋 𝒌 𝒍 𝒎 𝒏 𝒐 𝒑 𝒒 𝒓 𝒔 𝒕 𝒖 𝒗 𝒘 𝒙 𝒚 𝒛
   \bisans  𝙖 𝙗 𝙘 𝙙 𝙚 𝙛 𝙜 𝙝 𝙞 𝙟 𝙠 𝙡 𝙢 𝙣 𝙤 𝙥 𝙦 𝙧 𝙨 𝙩 𝙪 𝙫 𝙬 𝙭 𝙮 𝙯
   \bscr    𝓪 𝓫 𝓬 𝓭 𝓮 𝓯 𝓰 𝓱 𝓲 𝓳 𝓴 𝓵 𝓶 𝓷 𝓸 𝓹 𝓺 𝓻 𝓼 𝓽 𝓾 𝓿 𝔀 𝔁 𝔂 𝔃
   \bb      𝕒 𝕓 𝕔 𝕕 𝕖 𝕗 𝕘 𝕙 𝕚 𝕛 𝕜 𝕝 𝕞 𝕟 𝕠 𝕡 𝕢 𝕣 𝕤 𝕥 𝕦 𝕧 𝕨 𝕩 𝕪 𝕫
   \frak    𝔞 𝔟 𝔠 𝔡 𝔢 𝔣 𝔤 𝔥 𝔦 𝔧 𝔨 𝔩 𝔪 𝔫 𝔬 𝔭 𝔮 𝔯 𝔰 𝔱 𝔲 𝔳 𝔴 𝔵 𝔶 𝔷
   \bfrak   𝖆 𝖇 𝖈 𝖉 𝖊 𝖋 𝖌 𝖍 𝖎 𝖏 𝖐 𝖑 𝖒 𝖓 𝖔 𝖕 𝖖 𝖗 𝖘 𝖙 𝖚 𝖛 𝖜 𝖝 𝖞 𝖟
   """

   "Uppercase letters" =>
   raw"""
   regular  A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
   \sans    𝖠 𝖡 𝖢 𝖣 𝖤 𝖥 𝖦 𝖧 𝖨 𝖩 𝖪 𝖫 𝖬 𝖭 𝖮 𝖯 𝖰 𝖱 𝖲 𝖳 𝖴 𝖵 𝖶 𝖷 𝖸 𝖹
   \tt      𝙰 𝙱 𝙲 𝙳 𝙴 𝙵 𝙶 𝙷 𝙸 𝙹 𝙺 𝙻 𝙼 𝙽 𝙾 𝙿 𝚀 𝚁 𝚂 𝚃 𝚄 𝚅 𝚆 𝚇 𝚈 𝚉
   \it      𝐴 𝐵 𝐶 𝐷 𝐸 𝐹 𝐺 𝐻 𝐼 𝐽 𝐾 𝐿 𝑀 𝑁 𝑂 𝑃 𝑄 𝑅 𝑆 𝑇 𝑈 𝑉 𝑊 𝑋 𝑌 𝑍
   \isans   𝘈 𝘉 𝘊 𝘋 𝘌 𝘍 𝘎 𝘏 𝘐 𝘑 𝘒 𝘓 𝘔 𝘕 𝘖 𝘗 𝘘 𝘙 𝘚 𝘛 𝘜 𝘝 𝘞 𝘟 𝘠 𝘡
   \scr     𝒜 ℬ 𝒞 𝒟 ℰ ℱ 𝒢 ℋ ℐ 𝒥 𝒦 ℒ ℳ 𝒩 𝒪 𝒫 𝒬 ℛ 𝒮 𝒯 𝒰 𝒱 𝒲 𝒳 𝒴 𝒵
   \bf      𝐀 𝐁 𝐂 𝐃 𝐄 𝐅 𝐆 𝐇 𝐈 𝐉 𝐊 𝐋 𝐌 𝐍 𝐎 𝐏 𝐐 𝐑 𝐒 𝐓 𝐔 𝐕 𝐖 𝐗 𝐘 𝐙
   \bsans   𝗔 𝗕 𝗖 𝗗 𝗘 𝗙 𝗚 𝗛 𝗜 𝗝 𝗞 𝗟 𝗠 𝗡 𝗢 𝗣 𝗤 𝗥 𝗦 𝗧 𝗨 𝗩 𝗪 𝗫 𝗬 𝗭
   \bi      𝑨 𝑩 𝑪 𝑫 𝑬 𝑭 𝑮 𝑯 𝑰 𝑱 𝑲 𝑳 𝑴 𝑵 𝑶 𝑷 𝑸 𝑹 𝑺 𝑻 𝑼 𝑽 𝑾 𝑿 𝒀 𝒁
   \bisans  𝘼 𝘽 𝘾 𝘿 𝙀 𝙁 𝙂 𝙃 𝙄 𝙅 𝙆 𝙇 𝙈 𝙉 𝙊 𝙋 𝙌 𝙍 𝙎 𝙏 𝙐 𝙑 𝙒 𝙓 𝙔 𝙕
   \bscr    𝓐 𝓑 𝓒 𝓓 𝓔 𝓕 𝓖 𝓗 𝓘 𝓙 𝓚 𝓛 𝓜 𝓝 𝓞 𝓟 𝓠 𝓡 𝓢 𝓣 𝓤 𝓥 𝓦 𝓧 𝓨 𝓩
   \bb      𝔸 𝔹 ℂ 𝔻 𝔼 𝔽 𝔾 ℍ 𝕀 𝕁 𝕂 𝕃 𝕄 ℕ 𝕆 ℙ ℚ ℝ 𝕊 𝕋 𝕌 𝕍 𝕎 𝕏 𝕐 ℤ
   \frak    𝔄 𝔅 ℭ 𝔇 𝔈 𝔉 𝔊 ℌ ℑ 𝔍 𝔎 𝔏 𝔐 𝔑 𝔒 𝔓 𝔔 ℜ 𝔖 𝔗 𝔘 𝔙 𝔚 𝔛 𝔜 ℨ
   \bfrak   𝕬 𝕭 𝕮 𝕯 𝕰 𝕱 𝕲 𝕳 𝕴 𝕵 𝕶 𝕷 𝕸 𝕹 𝕺 𝕻 𝕼 𝕽 𝕾 𝕿 𝖀 𝖁 𝖂 𝖃 𝖄 𝖅
   """

   "Greek letters" =>
   raw"""
   regular α β γ δ ϵ ε ζ η θ ϑ ι κ ϰ λ μ ν ξ ο π ϖ ρ ϱ σ ς τ υ ϕ φ χ ψ ω ϝ ∂ ∇
   \it     𝛼 𝛽 𝛾 𝛿 𝜖 𝜀 𝜁 𝜂 𝜃 𝜗 𝜄 𝜅 𝜘 𝜆 𝜇 𝜈 𝜉 𝜊 𝜋 𝜛 𝜌 𝜚 𝜎 𝜍 𝜏 𝜐 𝜙 𝜑 𝜒 𝜓 𝜔   𝜕 𝛻
   \bf     𝛂 𝛃 𝛄 𝛅 𝛜 𝛆 𝛇 𝛈 𝛉 𝛝 𝛊 𝛋 𝛞 𝛌 𝛍 𝛎 𝛏 𝛐 𝛑 𝛡 𝛒 𝛠 𝛔 𝛓 𝛕 𝛖 𝛟 𝛗 𝛘 𝛙 𝛚 𝟋 𝛛 𝛁
   \bsans  𝝰 𝝱 𝝲 𝝳 𝞊 𝝴 𝝵 𝝶 𝝷 𝞋 𝝸 𝝹 𝞌 𝝺 𝝻 𝝼 𝝽 𝝾 𝝿 𝞏 𝞀 𝞎 𝞂 𝞁 𝞃 𝞄 𝞍 𝞅 𝞆 𝞇 𝞈   𝞉 𝝯
   \bi     𝜶 𝜷 𝜸 𝜹 𝝐 𝜺 𝜻 𝜼 𝜽 𝝑 𝜾 𝜿 𝝒 𝝀 𝝁 𝝂 𝝃 𝝄 𝝅 𝝕 𝝆 𝝔 𝝈 𝝇 𝝉 𝝊 𝝓 𝝋 𝝌 𝝍 𝝎   𝝏 𝜵
   \bisans 𝞪 𝞫 𝞬 𝞭 𝟄 𝞮 𝞯 𝞰 𝞱 𝟅 𝞲 𝞳 𝟆 𝞴 𝞵 𝞶 𝞷 𝞸 𝞹 𝟉 𝞺 𝟈 𝞼 𝞻 𝞽 𝞾 𝟇 𝞿 𝟀 𝟁 𝟂   𝟃 𝞩
   \bb         ℽ                               ℼ

   regular Α Β Γ Δ Ε   Ζ Η Θ ϴ Ι Κ   Λ Μ Ν Ξ Ο Π   Ρ   Σ   Τ Υ Φ   Χ Ψ Ω Ϝ
   \it     𝛢 𝛣 𝛤 𝛥 𝛦   𝛧 𝛨 𝛩 𝛳 𝛪 𝛫   𝛬 𝛭 𝛮 𝛯 𝛰 𝛱   𝛲   𝛴   𝛵 𝛶 𝛷   𝛸 𝛹 𝛺
   \bf     𝚨 𝚩 𝚪 𝚫 𝚬   𝚭 𝚮 𝚯 𝚹 𝚰 𝚱   𝚲 𝚳 𝚴 𝚵 𝚶 𝚷   𝚸   𝚺   𝚻 𝚼 𝚽   𝚾 𝚿 𝛀 𝟊
   \bsans  𝝖 𝝗 𝝘 𝝙 𝝚   𝝛 𝝜 𝝝 𝝧 𝝞 𝝟   𝝠 𝝡 𝝢 𝝣 𝝤 𝝥   𝝦   𝝨   𝝩 𝝪 𝝫   𝝬 𝝭 𝝮
   \bi     𝜜 𝜝 𝜞 𝜟 𝜠   𝜡 𝜢 𝜣 𝜭 𝜤 𝜥   𝜦 𝜧 𝜨 𝜩 𝜪 𝜫   𝜬   𝜮   𝜯 𝜰 𝜱   𝜲 𝜳 𝜴
   \bisans 𝞐 𝞑 𝞒 𝞓 𝞔   𝞕 𝞖 𝞗 𝞡 𝞘 𝞙   𝞚 𝞛 𝞜 𝞝 𝞞 𝞟   𝞠   𝞢   𝞣 𝞤 𝞥   𝞦 𝞧 𝞨
   \bb         ℾ                               ℿ
   """

   "Digits, superscripts and subscripts" =>
   raw"""
   regular 0 1 2 3 4 5 6 7 8 9
   \sans   𝟢 𝟣 𝟤 𝟥 𝟦 𝟧 𝟨 𝟩 𝟪 𝟫
   \tt     𝟶 𝟷 𝟸 𝟹 𝟺 𝟻 𝟼 𝟽 𝟾 𝟿
   \bf     𝟎 𝟏 𝟐 𝟑 𝟒 𝟓 𝟔 𝟕 𝟖 𝟗
   \bsans  𝟬 𝟭 𝟮 𝟯 𝟰 𝟱 𝟲 𝟳 𝟴 𝟵
   \bb     𝟘 𝟙 𝟚 𝟛 𝟜 𝟝 𝟞 𝟟 𝟠 𝟡

   \^      A ᵃ ᵇ ᶜ ᵈ ᵉ ᶠ ᵍ ʰ ⁱ ʲ ᵏ ˡ ᵐ ⁿ ᵒ ᵖ   ʳ ˢ ᵗ ᵘ ᵛ ʷ ˣ ʸ ᶻ Z
   \^      A ᴬ ᴮ   ᴰ ᴱ   ᴳ ᴴ ᴵ ᴶ ᴷ ᴸ ᴹ ᴺ ᴼ ᴾ   ᴿ   ᵀ ᵁ ⱽ ᵂ       Z
   \_      A ₐ       ₑ     ₕ ᵢ ⱼ ₖ ₗ ₘ ₙ ₒ ₚ   ᵣ ₛ ₜ ᵤ ᵥ   ₓ     Z

   \^      A ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾   ᵅ ᵝ ᵞ ᵟ ᵋ ᶿ ᶥ ᵠ ᶲ ᵡ Z
   \_      A ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₌ ₍ ₎ ₔ   ᵦ ᵧ ᵨ         ᵩ ᵪ Z
   """

   "Operators" =>
   """
   ∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏   ⨀ ⨁ ⨂ ⨃ ⨄ ⨅ ⨆ ⨇ ⨈ ⨉ ⨊ ⨋ ⨌ ⨍ ⨎ ⨏
   ∐ ∑ − ∓ ∔ ∕ ∖ ∗ ∘ ∙ √ ∛ ∜ ∝ ∞ ∟   ⨐ ⨑ ⨒ ⨓ ⨔ ⨕ ⨖ ⨗ ⨘ ⨙ ⨚ ⨛ ⨜ ⨝ ⨞ ⨟
   ∠ ∡ ∢ ∣ ∤ ∥ ∦ ∧ ∨ ∩ ∪ ∫ ∬ ∭ ∮ ∯   ⨠ ⨡ ⨢ ⨣ ⨤ ⨥ ⨦ ⨧ ⨨ ⨩ ⨪ ⨫ ⨬ ⨭ ⨮ ⨯
   ∰ ∱ ∲ ∳ ∴ ∵ ∶ ∷ ∸ ∹ ∺ ∻ ∼ ∽ ∾ ∿   ⨰ ⨱ ⨲ ⨳ ⨴ ⨵ ⨶ ⨷ ⨸ ⨹ ⨺ ⨻ ⨼ ⨽ ⨾ ⨿
   ≀ ≁ ≂ ≃ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≏   ⩀ ⩁ ⩂ ⩃ ⩄ ⩅ ⩆ ⩇ ⩈ ⩉ ⩊ ⩋ ⩌ ⩍ ⩎ ⩏
   ≐ ≑ ≒ ≓ ≔ ≕ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟   ⩐ ⩑ ⩒ ⩓ ⩔ ⩕ ⩖ ⩗ ⩘ ⩙ ⩚ ⩛ ⩜ ⩝ ⩞ ⩟
   ≠ ≡ ≢ ≣ ≤ ≥ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯   ⩠ ⩡ ⩢ ⩣ ⩤ ⩥ ⩦ ⩧ ⩨ ⩩ ⩪ ⩫ ⩬ ⩭ ⩮ ⩯
   ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿   ⩰ ⩱ ⩲ ⩳ ⩴ ⩵ ⩶ ⩷ ⩸ ⩹ ⩺ ⩻ ⩼ ⩽ ⩾ ⩿
   ⊀ ⊁ ⊂ ⊃ ⊄ ⊅ ⊆ ⊇ ⊈ ⊉ ⊊ ⊋ ⊌ ⊍ ⊎ ⊏   ⪀ ⪁ ⪂ ⪃ ⪄ ⪅ ⪆ ⪇ ⪈ ⪉ ⪊ ⪋ ⪌ ⪍ ⪎ ⪏
   ⊐ ⊑ ⊒ ⊓ ⊔ ⊕ ⊖ ⊗ ⊘ ⊙ ⊚ ⊛ ⊜ ⊝ ⊞ ⊟   ⪐ ⪑ ⪒ ⪓ ⪔ ⪕ ⪖ ⪗ ⪘ ⪙ ⪚ ⪛ ⪜ ⪝ ⪞ ⪟
   ⊠ ⊡ ⊢ ⊣ ⊤ ⊥ ⊦ ⊧ ⊨ ⊩ ⊪ ⊫ ⊬ ⊭ ⊮ ⊯   ⪠ ⪡ ⪢ ⪣ ⪤ ⪥ ⪦ ⪧ ⪨ ⪩ ⪪ ⪫ ⪬ ⪭ ⪮ ⪯
   ⊰ ⊱ ⊲ ⊳ ⊴ ⊵ ⊶ ⊷ ⊸ ⊹ ⊺ ⊻ ⊼ ⊽ ⊾ ⊿   ⪰ ⪱ ⪲ ⪳ ⪴ ⪵ ⪶ ⪷ ⪸ ⪹ ⪺ ⪻ ⪼ ⪽ ⪾ ⪿
   ⋀ ⋁ ⋂ ⋃ ⋄ ⋅ ⋆ ⋇ ⋈ ⋉ ⋊ ⋋ ⋌ ⋍ ⋎ ⋏   ⫀ ⫁ ⫂ ⫃ ⫄ ⫅ ⫆ ⫇ ⫈ ⫉ ⫊ ⫋ ⫌ ⫍ ⫎ ⫏
   ⋐ ⋑ ⋒ ⋓ ⋔ ⋕ ⋖ ⋗ ⋘ ⋙ ⋚ ⋛ ⋜ ⋝ ⋞ ⋟   ⫐ ⫑ ⫒ ⫓ ⫔ ⫕ ⫖ ⫗ ⫘ ⫙ ⫚ ⫛ ⫝̸ ⫝ ⫞ ⫟
   ⋠ ⋡ ⋢ ⋣ ⋤ ⋥ ⋦ ⋧ ⋨ ⋩ ⋪ ⋫ ⋬ ⋭ ⋮ ⋯   ⫠ ⫡ ⫢ ⫣ ⫤ ⫥ ⫦ ⫧ ⫨ ⫩ ⫪ ⫫ ⫬ ⫭ ⫮ ⫯
   ⋰ ⋱ ⋲ ⋳ ⋴ ⋵ ⋶ ⋷ ⋸ ⋹ ⋺ ⋻ ⋼ ⋽ ⋾ ⋿   ⫰ ⫱ ⫲ ⫳ ⫴ ⫵ ⫶ ⫷ ⫸ ⫹ ⫺ ⫻ ⫼ ⫽ ⫾ ⫿
   """

   "Letterlike and miscellanous symbols, arrows" =>
   """
   ℀ ℁ ℂ ℃ ℄ ℅ ℆ ℇ ℈ ℉ ℊ ℋ ℌ ℍ ℎ ℏ   ⦀ ⦁ ⦂ ⦃ ⦄ ⦅ ⦆ ⦇ ⦈ ⦉ ⦊ ⦋ ⦌ ⦍ ⦎ ⦏
   ℐ ℑ ℒ ℓ ℔ ℕ № ℗ ℘ ℙ ℚ ℛ ℜ ℝ ℞ ℟   ⦐ ⦑ ⦒ ⦓ ⦔ ⦕ ⦖ ⦗ ⦘ ⦙ ⦚ ⦛ ⦜ ⦝ ⦞ ⦟
   ℠ ℡ ™ ℣ ℤ ℥ Ω ℧ ℨ ℩ K Å ℬ ℭ ℮ ℯ   ⦠ ⦡ ⦢ ⦣ ⦤ ⦥ ⦦ ⦧ ⦨ ⦩ ⦪ ⦫ ⦬ ⦭ ⦮ ⦯
   ℰ ℱ Ⅎ ℳ ℴ ℵ ℶ ℷ ℸ ℹ ℺ ℻ ℼ ℽ ℾ ℿ   ⦰ ⦱ ⦲ ⦳ ⦴ ⦵ ⦶ ⦷ ⦸ ⦹ ⦺ ⦻ ⦼ ⦽ ⦾ ⦿
   ⅀ ⅁ ⅂ ⅃ ⅄ ⅅ ⅆ ⅇ ⅈ ⅉ ⅊ ⅋ ⅌ ⅍ ⅎ ⅏   ⧀ ⧁ ⧂ ⧃ ⧄ ⧅ ⧆ ⧇ ⧈ ⧉ ⧊ ⧋ ⧌ ⧍ ⧎ ⧏
   ⟀ ⟁ ⟂ ⟃ ⟄ ⟅ ⟆ ⟇ ⟈ ⟉ ⟊ ⟋ ⟌ ⟍ ⟎ ⟏   ⧐ ⧑ ⧒ ⧓ ⧔ ⧕ ⧖ ⧗ ⧘ ⧙ ⧚ ⧛ ⧜ ⧝ ⧞ ⧟
   ⟐ ⟑ ⟒ ⟓ ⟔ ⟕ ⟖ ⟗ ⟘ ⟙ ⟚ ⟛ ⟜ ⟝ ⟞ ⟟   ⧠ ⧡ ⧢ ⧣ ⧤ ⧥ ⧦ ⧧ ⧨ ⧩ ⧪ ⧫ ⧬ ⧭ ⧮ ⧯
   ⟠ ⟡ ⟢ ⟣ ⟤ ⟥ ⟦ ⟧ ⟨ ⟩ ⟪ ⟫ ⟬ ⟭ ⟮ ⟯   ⧰ ⧱ ⧲ ⧳ ⧴ ⧵ ⧶ ⧷ ⧸ ⧹ ⧺ ⧻ ⧼ ⧽ ⧾ ⧿

   ← ↑ → ↓ ↔ ↕ ↖ ↗ ↘ ↙ ↚ ↛ ↜ ↝ ↞ ↟   ⤀ ⤁ ⤂ ⤃ ⤄ ⤅ ⤆ ⤇ ⤈ ⤉ ⤊ ⤋ ⤌ ⤍ ⤎ ⤏
   ↠ ↡ ↢ ↣ ↤ ↥ ↦ ↧ ↨ ↩ ↪ ↫ ↬ ↭ ↮ ↯   ⤐ ⤑ ⤒ ⤓ ⤔ ⤕ ⤖ ⤗ ⤘ ⤙ ⤚ ⤛ ⤜ ⤝ ⤞ ⤟
   ↰ ↱ ↲ ↳ ↴ ↵ ↶ ↷ ↸ ↹ ↺ ↻ ↼ ↽ ↾ ↿   ⤠ ⤡ ⤢ ⤣ ⤤ ⤥ ⤦ ⤧ ⤨ ⤩ ⤪ ⤫ ⤬ ⤭ ⤮ ⤯
   ⇀ ⇁ ⇂ ⇃ ⇄ ⇅ ⇆ ⇇ ⇈ ⇉ ⇊ ⇋ ⇌ ⇍ ⇎ ⇏   ⤰ ⤱ ⤲ ⤳ ⤴ ⤵ ⤶ ⤷ ⤸ ⤹ ⤺ ⤻ ⤼ ⤽ ⤾ ⤿
   ⇐ ⇑ ⇒ ⇓ ⇔ ⇕ ⇖ ⇗ ⇘ ⇙ ⇚ ⇛ ⇜ ⇝ ⇞ ⇟   ⥀ ⥁ ⥂ ⥃ ⥄ ⥅ ⥆ ⥇ ⥈ ⥉ ⥊ ⥋ ⥌ ⥍ ⥎ ⥏
   ⇠ ⇡ ⇢ ⇣ ⇤ ⇥ ⇦ ⇧ ⇨ ⇩ ⇪ ⇫ ⇬ ⇭ ⇮ ⇯   ⥐ ⥑ ⥒ ⥓ ⥔ ⥕ ⥖ ⥗ ⥘ ⥙ ⥚ ⥛ ⥜ ⥝ ⥞ ⥟
   ⇰ ⇱ ⇲ ⇳ ⇴ ⇵ ⇶ ⇷ ⇸ ⇹ ⇺ ⇻ ⇼ ⇽ ⇾ ⇿   ⥠ ⥡ ⥢ ⥣ ⥤ ⥥ ⥦ ⥧ ⥨ ⥩ ⥪ ⥫ ⥬ ⥭ ⥮ ⥯
   ⟰ ⟱ ⟲ ⟳ ⟴ ⟵ ⟶ ⟷ ⟸ ⟹ ⟺ ⟻ ⟼ ⟽ ⟾ ⟿   ⥰ ⥱ ⥲ ⥳ ⥴ ⥵ ⥶ ⥷ ⥸ ⥹ ⥺ ⥻ ⥼ ⥽ ⥾ ⥿
   """

   "Miscellanous technical symbols, miscellanous symbols and arrows, geometric shapes, other symbols" =>
   """
   ⌀ ⌁ ⌂ ⌃ ⌄ ⌅ ⌆ ⌇ ⌈ ⌉ ⌊ ⌋ ⌌ ⌍ ⌎ ⌏   ⬀ ⬁ ⬂ ⬃ ⬄ ⬅ ⬆ ⬇ ⬈ ⬉ ⬊ ⬋ ⬌ ⬍ ⬎ ⬏
   ⌐ ⌑ ⌒ ⌓ ⌔ ⌕ ⌖ ⌗ ⌘ ⌙ ⌚ ⌛ ⌜ ⌝ ⌞ ⌟   ⬐ ⬑ ⬒ ⬓ ⬔ ⬕ ⬖ ⬗ ⬘ ⬙ ⬚ ⬛ ⬜ ⬝ ⬞ ⬟
   ⌠ ⌡ ⌢ ⌣ ⌤ ⌥ ⌦ ⌧ ⌨ 〈 〉 ⌫ ⌬ ⌭ ⌮ ⌯   ⬠ ⬡ ⬢ ⬣ ⬤ ⬥ ⬦ ⬧ ⬨ ⬩ ⬪ ⬫ ⬬ ⬭ ⬮ ⬯
   ⌰ ⌱ ⌲ ⌳ ⌴ ⌵ ⌶ ⌷ ⌸ ⌹ ⌺ ⌻ ⌼ ⌽ ⌾ ⌿   ⬰ ⬱ ⬲ ⬳ ⬴ ⬵ ⬶ ⬷ ⬸ ⬹ ⬺ ⬻ ⬼ ⬽ ⬾ ⬿
   ⍀ ⍁ ⍂ ⍃ ⍄ ⍅ ⍆ ⍇ ⍈ ⍉ ⍊ ⍋ ⍌ ⍍ ⍎ ⍏   ⭀ ⭁ ⭂ ⭃ ⭄ ⭅ ⭆ ⭇ ⭈ ⭉ ⭊ ⭋ ⭌ ⭍ ⭎ ⭏
   ⍐ ⍑ ⍒ ⍓ ⍔ ⍕ ⍖ ⍗ ⍘ ⍙ ⍚ ⍛ ⍜ ⍝ ⍞ ⍟   ⭐ ⭑ ⭒ ⭓ ⭔ ⭕ ⭖ ⭗ ⭘ ⭙ ⭚ ⭛ ⭜ ⭝ ⭞ ⭟
   ⍠ ⍡ ⍢ ⍣ ⍤ ⍥ ⍦ ⍧ ⍨ ⍩ ⍪ ⍫ ⍬ ⍭ ⍮ ⍯   ⭠ ⭡ ⭢ ⭣ ⭤ ⭥ ⭦ ⭧ ⭨ ⭩ ⭪ ⭫ ⭬ ⭭ ⭮ ⭯
   ⍰ ⍱ ⍲ ⍳ ⍴ ⍵ ⍶ ⍷ ⍸ ⍹ ⍺ ⍻ ⍼ ⍽ ⍾ ⍿   ⭰ ⭱ ⭲ ⭳   ⭶ ⭷ ⭸ ⭹ ⭺ ⭻ ⭼ ⭽ ⭾ ⭿
   ⎀ ⎁ ⎂ ⎃ ⎄ ⎅ ⎆ ⎇ ⎈ ⎉ ⎊ ⎋ ⎌ ⎍ ⎎ ⎏   ⮀ ⮁ ⮂ ⮃ ⮄ ⮅ ⮆ ⮇ ⮈ ⮉ ⮊ ⮋ ⮌ ⮍ ⮎ ⮏
   ⎐ ⎑ ⎒ ⎓ ⎔ ⎕ ⎖ ⎗ ⎘ ⎙ ⎚ ⎛ ⎜ ⎝ ⎞ ⎟   ⮐ ⮑ ⮒ ⮓ ⮔ ⮕  ⮗ ⮘ ⮙ ⮚ ⮛ ⮜ ⮝ ⮞ ⮟
   ⎠ ⎡ ⎢ ⎣ ⎤ ⎥ ⎦ ⎧ ⎨ ⎩ ⎪ ⎫ ⎬ ⎭ ⎮ ⎯   ⮠ ⮡ ⮢ ⮣ ⮤ ⮥ ⮦ ⮧ ⮨ ⮩ ⮪ ⮫ ⮬ ⮭ ⮮ ⮯
   ⎰ ⎱ ⎲ ⎳ ⎴ ⎵ ⎶ ⎷ ⎸ ⎹ ⎺ ⎻ ⎼ ⎽ ⎾ ⎿   ⮰ ⮱ ⮲ ⮳ ⮴ ⮵ ⮶ ⮷ ⮸ ⮹ ⮺ ⮻ ⮼ ⮽ ⮾ ⮿
   ⏀ ⏁ ⏂ ⏃ ⏄ ⏅ ⏆ ⏇ ⏈ ⏉ ⏊ ⏋ ⏌ ⏍ ⏎ ⏏   ⯀ ⯁ ⯂ ⯃ ⯄ ⯅ ⯆ ⯇ ⯈ ⯉ ⯊ ⯋ ⯌ ⯍ ⯎ ⯏
   ⏐ ⏑ ⏒ ⏓ ⏔ ⏕ ⏖ ⏗ ⏘ ⏙ ⏚ ⏛ ⏜ ⏝ ⏞ ⏟   ⯐ ⯑ ⯒ ⯓ ⯔ ⯕ ⯖ ⯗ ⯘ ⯙ ⯚ ⯛ ⯜ ⯝ ⯞ ⯟
   ⏠ ⏡ ⏢ ⏣ ⏤ ⏥ ⏦ ⏧ ⏨ ⏩ ⏪ ⏫ ⏬ ⏭ ⏮ ⏯   ⯠ ⯡ ⯢ ⯣ ⯤ ⯥ ⯦ ⯧ ⯨ ⯩ ⯪ ⯫ ⯬ ⯭ ⯮ ⯯
   ⏰ ⏱ ⏲ ⏳ ⏴ ⏵ ⏶ ⏷ ⏸ ⏹ ⏺ ⏻ ⏼ ⏽ ⏾ ⏿   ⯰ ⯱ ⯲ ⯳ ⯴ ⯵ ⯶ ⯷ ⯸ ⯹ ⯺ ⯻ ⯼ ⯽ ⯾ ⯿

   ■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯   + - < = > ^ | ~ ¬ ± × ÷
   ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿   ★ ☆ ♀ ♂ ♠ ♡ ♢ ♣ ♭ ♮ ♯
   ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ●   ﹐ ﹑ ﹒ ﹔ ﹕ ﹖ ﹗ ﹘ ﹙ ﹚ ﹛ ﹜ ﹝ ﹞ ﹟
   ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟   ﹠ ﹡ ﹢ ﹣ ﹤ ﹥ ﹦ ﹨ ﹩ ﹪ ﹫
   ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯   ＋ ＜ ＝ ＞ ＼ ＾ ｜ ～ ￢   ￩ ￪ ￫ ￬   ﬩
   ◰ ◱ ◲ ◳ ◴ ◵ ◶ ◷ ◸ ◹ ◺ ◻ ◼ ◽ ◾ ◿   ‖ ′ ″ ‴ ⁀ ⁄ ⁒ ϐ ϑ ϒ ϕ ϰ ϱ ϴ ϵ ϶
   """

   "Arabic mathematical symbols" =>
   """
   𞸀	𞸁	𞸂	𞸃		𞸅	𞸆	𞸇	𞸈	𞸉	𞸊	𞸋	𞸌	𞸍	𞸎	𞸏
   𞸐	𞸑	𞸒	𞸓	𞸔	𞸕	𞸖	𞸗	𞸘	𞸙	𞸚	𞸛	𞸜	𞸝	𞸞	𞸟
   	𞸡	𞸢		𞸤			𞸧		𞸩	𞸪	𞸫	𞸬	𞸭	𞸮	𞸯
   𞸰	𞸱	𞸲		𞸴	𞸵	𞸶	𞸷		𞸹		𞸻
   		𞹂					𞹇		𞹉		𞹋		𞹍	𞹎	𞹏
   	𞹑	𞹒		𞹔			𞹗		𞹙		𞹛		𞹝		𞹟
   	𞹡	𞹢		𞹤			𞹧	𞹨	𞹩	𞹪		𞹬	𞹭	𞹮	𞹯
   𞹰	𞹱	𞹲		𞹴	𞹵	𞹶	𞹷		𞹹	𞹺	𞹻	𞹼		𞹾
   𞺀	𞺁	𞺂	𞺃	𞺄	𞺅	𞺆	𞺇	𞺈	𞺉		𞺋	𞺌	𞺍	𞺎	𞺏
   𞺐	𞺑	𞺒	𞺓	𞺔	𞺕	𞺖	𞺗	𞺘	𞺙	𞺚	𞺛
   	𞺡	𞺢	𞺣		𞺥	𞺦	𞺧	𞺨	𞺩		𞺫	𞺬	𞺭	𞺮	𞺯
   𞺰	𞺱	𞺲	𞺳	𞺴	𞺵	𞺶	𞺷	𞺸	𞺹	𞺺	𞺻

   𞻰	𞻱	   ؆ ؇ ؈
   """
  ]

const extra_samples = [
   "∂⃖rrule" =>
   """
   # From https://gist.github.com/Keno/d8faa85dd64c878e0985e25942bce450

   # ∂⃖rrule has a 4-recurrence - we model this as 4 separate structs that we
   # cycle between. N.B.: These names match the names that these variables
   # have in Snippet 19 of the terminology guide. They are probably not ideal,
   # but if you rename them here, please update the terminology guide also.

   struct ∂⃖rruleA{N, O}; ∂; ȳ; ȳ̄ ; end
   struct ∂⃖rruleB{N, O}; ᾱ; ȳ̄ ; end
   struct ∂⃖rruleC{N, O}; ȳ̄ ; Δ′′′; β̄ ; end
   struct ∂⃖rruleD{N, O}; γ̄; β̄ ; end

   function (a::∂⃖rruleA{N, O})(Δ) where {N, O}
   @destruct (α, ᾱ) = a.∂(a.ȳ, Δ)
   (α, ∂⃖rruleB{N, O}(ᾱ, a.ȳ̄))
   end

   function (b::∂⃖rruleB{N, O})(Δ′...) where {N, O}
   @destruct ((Δ′′′, β), β̄) = b.ᾱ(Δ′)
   (β, ∂⃖rruleC{N, O}(b.ȳ̄, Δ′′′, β̄))
   end

   function (c::∂⃖rruleC{N, O})(Δ′′) where {N, O}
   @destruct (γ, γ̄) = c.ȳ̄((Δ′′, c.Δ′′′))
   (Base.tail(γ), ∂⃖rruleD{N, O}(γ̄, c.β̄))
   end

   function (d::∂⃖rruleD{N, O})(Δ⁴...) where {N, O}
   (δ₁, δ₂), δ̄  = d.γ̄(Zero(), Δ⁴...)
   (δ₁, ∂⃖rruleA{N, O+1}(d.β̄ , δ₂, δ̄ ))
   end

   # Terminal cases
   function (c::∂⃖rruleB{N, N})(Δ′...) where {N}
   @destruct (Δ′′′, β) = c.ᾱ(Δ′)
   (β, ∂⃖rruleC{N, N}(c.ȳ̄, Δ′′′, nothing))
   end
   (c::∂⃖rruleC{N, N})(Δ′′) where {N} = Base.tail(c.ȳ̄((Δ′′, c.Δ′′′)))
   (::∂⃖rruleD{N, N})(Δ...) where {N} = error("Should not be reached")

   # ∂⃖rrule
   @Base.pure term_depth(N) = 2^(N-2)
   function (::∂⃖rrule{N})(z, z̄) where {N}
   @destruct (y, ȳ) = z
   y, ∂⃖rruleA{term_depth(N), 1}(∂⃖{minus1(N)}(), ȳ, z̄)
   end
   """
  ]

all_samples = [text_samples; extra_samples]

ext: jl

max_stars_repo_name: waldyrious/mono-math

max_issues_repo_name: waldyrious/mono-math

max_forks_repo_name: waldyrious/mono-math


text:
@testset "Charlwood Problems" begin
    (x, ) = @variables x

    #= ::Package:: =#

    #= ::Title:: =#
    #=Kevin*Charlwood - Integration*on*Computer*Algebra*Systems*(2008)=#

    #= ::Subsection::Closed:: =#
    #=Problem #1=#

    @test_int [asin(x)*log(x), x, 8, -2*sqrt(1 - x^2) + atanh(sqrt(1 - x^2)) - x*asin(x)*(1 - log(x)) + sqrt(1 - x^2)*log(x), -2*sqrt(1 - x^2) - x*asin(x) + atanh(sqrt(1 - x^2)) + sqrt(1 - x^2)*log(x) + x*asin(x)*log(x)]

    #= ::Subsection::Closed:: =#
    #=Problem #2=#

    @test_int [x*asin(x)/sqrt(1 - x^2), x, 2, x - sqrt(1 - x^2)*asin(x)]

    #= ::Subsection::Closed:: =#
    #=Problem #3=#

    @test_int [asin(sqrt(x + 1) - sqrt(x)), x, -3, ((sqrt(x) + 3*sqrt(1 + x))*sqrt(-x + sqrt(x)*sqrt(1 + x)))/(4*sqrt(2)) - (3/8 + x)*asin(sqrt(x) - sqrt(1 + x))]

    #= ::Subsection::Closed:: =#
    #=Problem #4=#

    @test_int [log(1 + x*sqrt(1 + x^2)), x, -32, -2*x + sqrt(2*(1 + sqrt(5)))*atan(sqrt(-2 + sqrt(5))*(x + sqrt(1 + x^2))) - sqrt(2*(-1 + sqrt(5)))*atanh(sqrt(2 + sqrt(5))*(x + sqrt(1 + x^2))) + x*log(1 + x*sqrt(1 + x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #5=#

    @test_int [cos(x)^2/sqrt(cos(x)^4 + cos(x)^2 + 1), x, -5, x/3 + (1/3)*atan((cos(x)*(1 + cos(x)^2)*sin(x))/(1 + cos(x)^2*sqrt(1 + cos(x)^2 + cos(x)^4)))]

    #= ::Subsection::Closed:: =#
    #=Problem #6=#

    @test_int [tan(x)*sqrt(1 + tan(x)^4), x, 7, (-(1/2))*asinh(tan(x)^2) - atanh((1 - tan(x)^2)/(sqrt(2)*sqrt(1 + tan(x)^4)))/sqrt(2) + (1/2)*sqrt(1 + tan(x)^4)]

    #= ::Subsection::Closed:: =#
    #=Problem #7=#

    @test_int [tan(x)/sqrt(1 + sec(x)^3), x, 4, (-(2/3))*atanh(sqrt(1 + sec(x)^3))]

    #= ::Subsection::Closed:: =#
    #=Problem #8=#

    @test_int [sqrt(tan(x)^2 + 2*tan(x) + 2), x, 9, asinh(1 + tan(x)) - sqrt((1/2)*(1 + sqrt(5)))*atan((2*sqrt(5) - (5 + sqrt(5))*tan(x))/(sqrt(10*(1 + sqrt(5)))*sqrt(2 + 2*tan(x) + tan(x)^2))) - sqrt((1/2)*(-1 + sqrt(5)))*atanh((2*sqrt(5) + (5 - sqrt(5))*tan(x))/(sqrt(10*(-1 + sqrt(5)))*sqrt(2 + 2*tan(x) + tan(x)^2)))]

    #= ::Subsection::Closed:: =#
    #=Problem #9=#

    @test_int [sin(x)*atan(sqrt(sec(x) - 1)), x, 7, (1/2)*atan(sqrt(-1 + sec(x))) - atan(sqrt(-1 + sec(x)))*cos(x) + (1/2)*cos(x)*sqrt(-1 + sec(x))]

    #= ::Subsection::Closed:: =#
    #=Problem #10=#

    @test_int [x^3*ℯ^asin(x)/sqrt(1 - x^2), x, 5, (1/10)*ℯ^asin(x)*(3*x + x^3 - 3*sqrt(1 - x^2) - 3*x^2*sqrt(1 - x^2)), (3/10)*ℯ^asin(x)*x + (1/10)*ℯ^asin(x)*x^3 - (3/10)*ℯ^asin(x)*sqrt(1 - x^2) - (3/10)*ℯ^asin(x)*x^2*sqrt(1 - x^2)]

    #= ::Subsection::Closed:: =#
    #=Problem #11=#

    @test_int [(x*log(1 + x^2)*log(x + sqrt(1 + x^2)))/sqrt(1 + x^2), x, 7, 4*x - 2*atan(x) - x*log(1 + x^2) - 2*sqrt(1 + x^2)*log(x + sqrt(1 + x^2)) + sqrt(1 + x^2)*log(1 + x^2)*log(x + sqrt(1 + x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #12=#

    @test_int [atan(x + sqrt(1 - x^2)), x, -40, -(asin(x)/2) + (1/4)*sqrt(3)*atan((-1 + sqrt(3)*x)/sqrt(1 - x^2)) + (1/4)*sqrt(3)*atan((1 + sqrt(3)*x)/sqrt(1 - x^2)) - (1/4)*sqrt(3)*atan((-1 + 2*x^2)/sqrt(3)) + x*atan(x + sqrt(1 - x^2)) - (1/4)*atanh(x*sqrt(1 - x^2)) - (1/8)*log(1 - x^2 + x^4)]

    #= ::Subsection::Closed:: =#
    #=Problem #13=#

    @test_int [x*atan(x + sqrt(1 - x^2))/sqrt(1 - x^2), x, -32, -(asin(x)/2) + (1/4)*sqrt(3)*atan((-1 + sqrt(3)*x)/sqrt(1 - x^2)) + (1/4)*sqrt(3)*atan((1 + sqrt(3)*x)/sqrt(1 - x^2)) - (1/4)*sqrt(3)*atan((-1 + 2*x^2)/sqrt(3)) - sqrt(1 - x^2)*atan(x + sqrt(1 - x^2)) + (1/4)*atanh(x*sqrt(1 - x^2)) + (1/8)*log(1 - x^2 + x^4)]

    #= ::Subsection::Closed:: =#
    #=Problem #14=#

    @test_int [asin(x)/(1 + sqrt(1 - x^2)), x, 9, -((x*asin(x))/(1 + sqrt(1 - x^2))) + asin(x)^2/2 - log(1 + sqrt(1 - x^2)), -(asin(x)/x) + (sqrt(1 - x^2)*asin(x))/x + asin(x)^2/2 - atanh(sqrt(1 - x^2)) - log(x)]

    #= ::Subsection::Closed:: =#
    #=Problem #15=#

    @test_int [log(x + sqrt(1 + x^2))/(1 - x^2)^(3/2), x, 3, (-(1/2))*asin(x^2) + (x*log(x + sqrt(1 + x^2)))/sqrt(1 - x^2)]

    #= ::Subsection::Closed:: =#
    #=Problem #16=#

    @test_int [asin(x)/(1 + x^2)^(3/2), x, 3, (x*asin(x))/sqrt(1 + x^2) - asin(x^2)/2]

    #= ::Subsection::Closed:: =#
    #=Problem #17=#

    @test_int [log(x + sqrt(x^2 - 1))/(1 + x^2)^(3/2), x, 3, (-(1/2))*acosh(x^2) + (x*log(x + sqrt(-1 + x^2)))/sqrt(1 + x^2)]

    #= ::Subsection::Closed:: =#
    #=Problem #18=#

    @test_int [log(x)/(x^2*sqrt(x^2 - 1)), x, 4, sqrt(-1 + x^2)/x - atanh(x/sqrt(-1 + x^2)) + (sqrt(-1 + x^2)*log(x))/x]

    #= ::Subsection::Closed:: =#
    #=Problem #19=#

    @test_int [sqrt(1 + x^3)/x, x, 4, (2*sqrt(1 + x^3))/3 - (2/3)*atanh(sqrt(1 + x^3))]

    #= ::Subsection::Closed:: =#
    #=Problem #20=#

    @test_int [x*log(x + sqrt(x^2 - 1))/sqrt(x^2 - 1), x, 2, -x + sqrt(-1 + x^2)*log(x + sqrt(-1 + x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #21=#

    @test_int [x^3*(asin(x)/sqrt(1 - x^4)), x, 5, (1/4)*x*sqrt(1 + x^2) - (1/2)*sqrt(1 - x^4)*asin(x) + asinh(x)/4]

    #= ::Subsection::Closed:: =#
    #=Problem #22=#

    @test_int [x^3*(asec(x)/sqrt(x^4 - 1)), x, 7, -(sqrt(-1 + x^4)/(2*sqrt(1 - 1/x^2)*x)) + (1/2)*sqrt(-1 + x^4)*asec(x) + (1/2)*atanh((sqrt(1 - 1/x^2)*x)/sqrt(-1 + x^4)), -(sqrt(-1 + x^4)/(2*sqrt(1 - 1/x^2)*x)) + (1/2)*sqrt(-1 + x^4)*asec(x) + (sqrt(1 - x^2)*atan(sqrt(-1 + x^4)/sqrt(1 - x^2)))/(2*sqrt(1 - 1/x^2)*x)]

    #= ::Subsection::Closed:: =#
    #=Problem #23=#

    @test_int [x*atan(x)*log(x + sqrt(1 + x^2))/sqrt(1 + x^2), x, 4, (-x)*atan(x) + (1/2)*log(1 + x^2) + sqrt(1 + x^2)*atan(x)*log(x + sqrt(1 + x^2)) - (1/2)*log(x + sqrt(1 + x^2))^2]

    #= ::Subsection::Closed:: =#
    #=Problem #24=#

    @test_int [x*log(1 + sqrt(1 - x^2))/sqrt(1 - x^2), x, 5, sqrt(1 - x^2) - log(1 + sqrt(1 - x^2)) - sqrt(1 - x^2)*log(1 + sqrt(1 - x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #25=#

    @test_int [x*log(x + sqrt(1 + x^2))/sqrt(1 + x^2), x, 2, -x + sqrt(1 + x^2)*log(x + sqrt(1 + x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #26=#

    @test_int [x*log(x + sqrt(1 - x^2))/sqrt(1 - x^2), x, 18, sqrt(1 - x^2) + atanh(sqrt(2)*x)/sqrt(2) - atanh(sqrt(2)*sqrt(1 - x^2))/sqrt(2) - sqrt(1 - x^2)*log(x + sqrt(1 - x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #27=#

    @test_int [log(x)/(x^2*sqrt(1 - x^2)), x, 3, -(sqrt(1 - x^2)/x) - asin(x) - (sqrt(1 - x^2)*log(x))/x]

    #= ::Subsection::Closed:: =#
    #=Problem #28=#

    @test_int [x*atan(x)/sqrt(1 + x^2), x, 2, -asinh(x) + sqrt(1 + x^2)*atan(x)]

    #= ::Subsection::Closed:: =#
    #=Problem #29=#

    @test_int [atan(x)/(x^2*sqrt(1 - x^2)), x, 7, -((sqrt(1 - x^2)*atan(x))/x) - atanh(sqrt(1 - x^2)) + sqrt(2)*atanh(sqrt(1 - x^2)/sqrt(2))]

    #= ::Subsection::Closed:: =#
    #=Problem #30=#

    @test_int [x*atan(x)/sqrt(1 - x^2), x, 5, -asin(x) - sqrt(1 - x^2)*atan(x) + sqrt(2)*atan((sqrt(2)*x)/sqrt(1 - x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #31=#

    @test_int [atan(x)/(x^2*sqrt(1 + x^2)), x, 4, -((sqrt(1 + x^2)*atan(x))/x) - atanh(sqrt(1 + x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #32=#

    @test_int [asin(x)/(x^2*sqrt(1 - x^2)), x, 2, -((sqrt(1 - x^2)*asin(x))/x) + log(x)]

    #= ::Subsection::Closed:: =#
    #=Problem #33=#

    @test_int [x*log(x)/sqrt(x^2 - 1), x, 5, -sqrt(-1 + x^2) + atan(sqrt(-1 + x^2)) + sqrt(-1 + x^2)*log(x)]

    #= ::Subsection::Closed:: =#
    #=Problem #34=#

    @test_int [log(x)/(x^2*sqrt(1 + x^2)), x, 3, -(sqrt(1 + x^2)/x) + asinh(x) - (sqrt(1 + x^2)*log(x))/x]

    #= ::Subsection::Closed:: =#
    #=Problem #35=#

    @test_int [x*asec(x)/sqrt(x^2 - 1), x, 2, sqrt(-1 + x^2)*asec(x) - (x*log(x))/sqrt(x^2)]

    #= ::Subsection::Closed:: =#
    #=Problem #36=#

    @test_int [x*log(x)/sqrt(1 + x^2), x, 5, -sqrt(1 + x^2) + atanh(sqrt(1 + x^2)) + sqrt(1 + x^2)*log(x)]

    #= ::Subsection::Closed:: =#
    #=Problem #37=#

    @test_int [sin(x)/(1 + sin(x)^2), x, 2, -(atanh(cos(x)/sqrt(2))/sqrt(2))]

    #= ::Subsection::Closed:: =#
    #=Problem #38=#

    @test_int [(1 + x^2)/((1 - x^2)*sqrt(1 + x^4)), x, 2, (1/sqrt(2))*atanh(sqrt(2)*(x/sqrt(1 + x^4)))]

    #= ::Subsection::Closed:: =#
    #=Problem #39=#

    @test_int [(1 - x^2)/((1 + x^2)*sqrt(1 + x^4)), x, 2, atan((sqrt(2)*x)/sqrt(1 + x^4))/sqrt(2)]

    #= ::Subsection::Closed:: =#
    #=Problem #40=#

    @test_int [log(sin(x))/(1 + sin(x)), x, 4, -x - atanh(cos(x)) - (cos(x)*log(sin(x)))/(1 + sin(x))]

    #= ::Subsection::Closed:: =#
    #=Problem #41=#

    @test_int [log(sin(x))*sqrt(1 + sin(x)), x, 6, (4*cos(x))/sqrt(1 + sin(x)) - (2*cos(x)*log(sin(x)))/sqrt(1 + sin(x)) - 4*atanh(cos(x)/sqrt(1 + sin(x)))]

    #= ::Subsection::Closed:: =#
    #=Problem #42=#

    @test_int [sec(x)/sqrt(sec(x)^4 - 1), x, -5, -(atanh((cos(x)*cot(x)*sqrt(sec(x)^4 - 1))/sqrt(2))/sqrt(2))]

    #= ::Subsection::Closed:: =#
    #=Problem #43=#

    @test_int [tan(x)/sqrt(1 + tan(x)^4), x, 4, -(atanh((1 - tan(x)^2)/(sqrt(2)*sqrt(1 + tan(x)^4)))/(2*sqrt(2)))]

    #= ::Subsection::Closed:: =#
    #=Problem #44=#

    @test_int [sin(x)/sqrt(1 - sin(x)^6), x, 4, atanh((sqrt(3)*cos(x)*(1 + sin(x)^2))/(2*sqrt(1 - sin(x)^6)))/(2*sqrt(3)), atanh((cos(x)*(6 - 3*cos(x)^2))/(2*sqrt(3)*sqrt(3*cos(x)^2 - 3*cos(x)^4 + cos(x)^6)))/(2*sqrt(3))]

    #= ::Subsection::Closed:: =#
    #=Problem #45=#

    @test_int [sqrt(sqrt(sec(x) + 1) - sqrt(sec(x) - 1)), x, -1, sqrt(2)*(sqrt(-1 + sqrt(2))*atan((sqrt(-2 + 2*sqrt(2))*(-sqrt(2) - sqrt(-1 + sec(x)) + sqrt(1 + sec(x))))/(2*sqrt(-sqrt(-1 + sec(x)) + sqrt(1 + sec(x))))) - sqrt(1 + sqrt(2))*atan((sqrt(2 + 2*sqrt(2))*(-sqrt(2) - sqrt(-1 + sec(x)) + sqrt(1 + sec(x))))/(2*sqrt(-sqrt(-1 + sec(x)) + sqrt(1 + sec(x))))) - sqrt(1 + sqrt(2))*atanh((sqrt(-2 + 2*sqrt(2))*sqrt(-sqrt(-1 + sec(x)) + sqrt(1 + sec(x))))/(sqrt(2) - sqrt(-1 + sec(x)) + sqrt(1 + sec(x)))) + sqrt(-1 + sqrt(2))*atanh((sqrt(2 + 2*sqrt(2))*sqrt(-sqrt(-1 + sec(x)) + sqrt(1 + sec(x))))/(sqrt(2) - sqrt(-1 + sec(x)) + sqrt(1 + sec(x)))))*cot(x)*sqrt(-1 + sec(x))*sqrt(1 + sec(x))]

    #= ::Subsection::Closed:: =#
    #=Problem #46=#

    @test_int [x*log(x^2 + 1)*atan(x)^2, x, 13, 3*x*atan(x) - (3*atan(x)^2)/2 - (1/2)*x^2*atan(x)^2 - (3/2)*log(1 + x^2) - x*atan(x)*log(1 + x^2) + (1/2)*(1 + x^2)*atan(x)^2*log(1 + x^2) + (1/4)*log(1 + x^2)^2]

    #= ::Subsection::Closed:: =#
    #=Problem #47=#

    @test_int [atan(x*sqrt(1 + x^2)), x, 12, x*atan(x*sqrt(1 + x^2)) + (1/2)*atan(sqrt(3) - 2*sqrt(1 + x^2)) - (1/2)*atan(sqrt(3) + 2*sqrt(1 + x^2)) - (1/4)*sqrt(3)*log(2 + x^2 - sqrt(3)*sqrt(1 + x^2)) + (1/4)*sqrt(3)*log(2 + x^2 + sqrt(3)*sqrt(1 + x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #48=#

    @test_int [atan(sqrt(x + 1) - sqrt(x)), x, 6, sqrt(x)/2 + (1 + x)*atan(sqrt(1 + x) - sqrt(x)), sqrt(x)/2 + (pi*x)/4 - atan(sqrt(x))/2 - (1/2)*x*atan(sqrt(x))]

    #= ::Subsection::Closed:: =#
    #=Problem #49=#

    @test_int [asin(x/sqrt(1 - x^2)), x, 4, x*asin(x/sqrt(1 - x^2)) + atan(sqrt(1 - 2*x^2))]

    #= ::Subsection::Closed:: =#
    #=Problem #50=#

    @test_int [atan(x*sqrt(1 - x^2)), x, 6, x*atan(x*sqrt(1 - x^2)) - sqrt((1/2)*(1 + sqrt(5)))*atan(sqrt((1/2)*(1 + sqrt(5)))*sqrt(1 - x^2)) + sqrt((1/2)*(-1 + sqrt(5)))*atanh(sqrt((1/2)*(-1 + sqrt(5)))*sqrt(1 - x^2)), (-sqrt(2/(-1 + sqrt(5))))*atan(sqrt(2/(-1 + sqrt(5)))*sqrt(1 - x^2)) + x*atan(x*sqrt(1 - x^2)) + sqrt(2/(1 + sqrt(5)))*atanh(sqrt(2/(1 + sqrt(5)))*sqrt(1 - x^2))]
    end

max_stars_repo_path: test/0 Independent test suites/Charlwood Problems.jl


text:
module DeriveColumns

using DataFrames, Query

#############################
## constant / static value ##
#############################

# new copy
# using Query - stand alone
hungry = person |>
    @mutate(hungry = "yes") |>
    DataFrame

# in-place
# using DataFrames
person[!, :hungry] .= "yes"

##################
## derive value ##
##################

# new copy
# using Query - stand alone
hungry_sa = person |>
    @mutate(hungry = (_.sweet_tooth == "yes" && lowercase(_.name[1]) in ['a', 'e', 'i', 'o', 'u']) ? "yes" : "no")

# new copy
# using Query - LINQ
hungry_q = @from i in person begin
    @select {i..., hungry = (i.sweet_tooth == "yes" && lowercase(i.name[1]) in ['a', 'e', 'i', 'o', 'u']) ? "yes" : "no"}
    @collect DataFrame
end

end # module

max_stars_repo_name: curtisalexander/saspyrilia-sandboxes

max_issues_repo_name: curtisalexander/saspyrilia-sandboxes

max_forks_repo_name: curtisalexander/saspyrilia-sandboxes


text:
"""
    axes_types(::Type{T}) -> Type{Tuple{Vararg{AbstractUnitRange{Int}}}}
    axes_types(::Type{T}, dim) -> Type{AbstractUnitRange{Int}}

Returns the type of each axis for the `T`, or the type of of the axis along dimension `dim`.
"""
@inline axes_types(x, dim) = axes_types(x, to_dims(x, dim))
@inline function axes_types(x, dim::StaticInt{D}) where {D}
    if D > ndims(x)
        return SOneTo{1}
    else
        return field_type(axes_types(x), dim)
    end
end
@inline function axes_types(x, dim::Int)
    if dim > ndims(x)
        return SOneTo{1}
    else
        return axes_types(x).parameters[dim]
    end
end
axes_types(x) = axes_types(typeof(x))
axes_types(::Type{T}) where {T<:Array} = NTuple{ndims(T),OneTo{Int}}
@inline function axes_types(::Type{T}) where {T}
    if parent_type(T) <: T
        return NTuple{ndims(T),OptionallyStaticUnitRange{One,Int}}
    else
        return axes_types(parent_type(T))
    end
end
axes_types(::Type{LinearIndices{N,R}}) where {N,R} = R
axes_types(::Type{CartesianIndices{N,R}}) where {N,R} = R
function axes_types(::Type{T}) where {T<:VecAdjTrans}
    Tuple{SOneTo{1},axes_types(parent_type(T), static(1))}
end
function axes_types(::Type{T}) where {T<:MatAdjTrans}
    Tuple{axes_types(parent_type(T), static(2)),axes_types(parent_type(T), static(1))}
end
function axes_types(::Type{T}) where {T<:PermutedDimsArray}
    eachop_tuple(field_type, to_parent_dims(T), axes_types(parent_type(T)))
end
function axes_types(::Type{T}) where {T<:AbstractRange}
    if known_length(T) === nothing
        return Tuple{OneTo{Int}}
    else
        return Tuple{SOneTo{known_length(T)}}
    end
end
axes_types(::Type{T}) where {T<:ReshapedArray} = NTuple{ndims(T),OneTo{Int}}
function _sub_axis_type(::Type{PA}, ::Type{I}, dim::StaticInt{D}) where {I<:Tuple,PA,D}
    IT = field_type(I, dim)
    if IT <: Base.Slice
        axes_types(field_type(PA, dim), static(1))
    else
        axes_types(IT, static(1))
    end
end
@inline function axes_types(::Type{T}) where {N,P,I,T<:SubArray{<:Any,N,P,I}}
    return eachop_tuple(_sub_axis_type, to_parent_dims(T), axes_types(P), I)
end

function axes_types(::Type{T}) where {T<:ReinterpretArray}
    eachop_tuple(_non_reshaped_axis_type, nstatic(Val(ndims(T))), T)
end

function _non_reshaped_axis_type(::Type{A}, d::StaticInt{D}) where {A,D}
    paxis = axes_types(parent_type(A), d)
    if D === 1
        if known_length(paxis) === nothing
            return paxis
        else
            return SOneTo{div(known_length(paxis) * sizeof(eltype(parent_type(A))), sizeof(eltype(A)))}
        end
    else
        return paxis
    end
end

# FUTURE NOTE: we avoid  `SOneTo(1)` when `axis(A, dim::Int)``. This is inended to decreases
# breaking changes for this adopting this method to situations where they clearly benefit
# from the propagation of static axes. This creates the somewhat awkward situation of
# conditionally typed (but inferrable) axes. It also means we can't depend on constant
# propagation to preserve statically sized axes. This should probably be addressed before
# merging into Base Julia.
"""
    axes(A) -> Tuple{Vararg{AbstractUnitRange{Int}}}
    axes(A, dim) -> AbstractUnitRange{Int}

Returns the axis associated with each dimension of `A` or dimension `dim`.
`ArrayInterface.axes(::AbstractArray)` behaves nearly identical to `Base.axes` with the
exception of a handful of types replace `Base.OneTo{Int}` with `ArrayInterface.SOneTo`. For
example, the axis along the first dimension of `Transpose{T,<:AbstractVector{T}}` and
`Adjoint{T,<:AbstractVector{T}}` can be represented by `SOneTo(1)`. Similarly,
`Base.ReinterpretArray`'s first axis may be statically sized.
"""
@inline axes(A) = Base.axes(A)
axes(A::ReshapedArray) = Base.axes(A)
axes(A::PermutedDimsArray) = permute(axes(parent(A)), to_parent_dims(A))
axes(A::MatAdjTrans) = permute(axes(parent(A)), to_parent_dims(A))
axes(A::VecAdjTrans) = (SOneTo{1}(), axes(parent(A), 1))
axes(A::SubArray) = map(Base.axes1, permute(A.indices, to_parent_dims(A)))

@inline axes(A, dim) = _axes(A, to_dims(A, dim))
@inline function _axes(A, dim::Int)
    if dim > ndims(A)
        return OneTo(1)
    else
        return getfield(axes(A), Int(dim))
    end
end
@inline function _axes(A, ::StaticInt{dim}) where {dim}
    if dim > ndims(A)
        return SOneTo{1}()
    else
        return getfield(axes(A), Int(dim))
    end
end

@inline function axes(A::SubArray, dim::Integer)
    if dim > ndims(A)
        return OneTo(1)
    else
        return axes(getindex(A.indices, to_parent_dims(A, to_dims(A, dim))), 1)
    end
end
@inline function axes(A::SubArray, ::StaticInt{dim}) where {dim}
    if dim > ndims(A)
        return SOneTo{1}()
    else
        return axes(getindex(A.indices, to_parent_dims(A, to_dims(A, dim))), 1)
    end
end

if isdefined(Base, :ReshapedReinterpretArray)
    function axes_types(::Type{A}) where {T,N,S,A<:Base.ReshapedReinterpretArray{T,N,S}}
        if sizeof(S) > sizeof(T)
            return merge_tuple_type(Tuple{SOneTo{div(sizeof(S), sizeof(T))}}, axes_types(parent_type(A)))
        elseif sizeof(S) < sizeof(T)
            P = parent_type(A)
            return eachop_tuple(field_type, tail(nstatic(Val(ndims(P)))), axes_types(P))
        else
            return axes_types(parent_type(A))
        end
    end
    @inline function axes(A::Base.ReshapedReinterpretArray{T,N,S}) where {T,N,S}
        if sizeof(S) > sizeof(T)
            return (SOneTo(div(sizeof(S), sizeof(T))), axes(parent(A))...)
        elseif sizeof(S) < sizeof(T)
            return tail(axes(parent(A)))
        else
            return axes(parent(A))
        end
    end
    @inline function axes(A::Base.ReshapedReinterpretArray{T,N,S}, dim) where {T,N,S}
        d = to_dims(A, dim)
        if sizeof(S) > sizeof(T)
            if d == 1
                return SOneTo(div(sizeof(S), sizeof(T)))
            else
                return axes(parent(A), d - static(1))
            end
        elseif sizeof(S) < sizeof(T)
            return axes(parent(A), d - static(1))
        else
            return axes(parent(A), d)
        end
    end
end

"""
    LazyAxis{N}(parent::AbstractArray)

A lazy representation of `axes(parent, N)`.
"""
struct LazyAxis{N,P} <: AbstractUnitRange{Int}
    parent::P

    function LazyAxis{N}(parent::P) where {N,P}
        N > 0 && return new{N::Int,P}(parent)
        throw_dim_error(parent, N)
    end
    @inline LazyAxis{:}(parent::P) where {P} = new{ifelse(ndims(P) === 1, 1, :),P}(parent)
end

@inline Base.parent(x::LazyAxis{N,P}) where {N,P} = axes(getfield(x, :parent), static(N))
@inline function Base.parent(x::LazyAxis{:,P}) where {P}
    return eachindex(IndexLinear(), getfield(x, :parent))
end

@inline parent_type(::Type{LazyAxis{N,P}}) where {N,P} = axes_types(P, static(N))
# TODO this approach to parent_type(::Type{LazyAxis{:}}) is a bit hacky. Something like
# LabelledArrays has a linear set of symbolic keys, which could be propagated through
# `to_indices` for key based indexing. However, there currently isn't a good way of handling
# that when the linear indices aren't linearly accessible through a child array (e.g, adjoint)
# For now we just make sure the linear elements are accurate.
parent_type(::Type{LazyAxis{:,P}}) where {P<:Array} = OneTo{Int}
@inline function parent_type(::Type{LazyAxis{:,P}}) where {P}
    if known_length(P) === nothing
        return OptionallyStaticUnitRange{StaticInt{1},Int}
    else
        return SOneTo{known_length(P)}
    end
end

Base.keys(x::LazyAxis) = keys(parent(x))

Base.IndexStyle(::Type{T}) where {T<:LazyAxis} = IndexStyle(parent_type(T))

can_change_size(::Type{LazyAxis{N,P}}) where {N,P} = can_change_size(P)

known_first(::Type{LazyAxis{N,P}}) where {N,P} = known_offsets(P, static(N))
known_first(::Type{LazyAxis{:,P}}) where {P} = 1
Base.firstindex(x::LazyAxis) = first(x)
@inline function Base.first(x::LazyAxis{N})::Int where {N}
    if known_first(x) === nothing
        return Int(offsets(parent(x), static(N)))
    else
        return Int(known_first(x))
    end
end
@inline function Base.first(x::LazyAxis{:})::Int
    if known_first(x) === nothing
        return first(parent(x))
    else
        return known_first(x)
    end
end
known_last(::Type{LazyAxis{N,P}}) where {N,P} = known_last(axes_types(P, static(N)))
known_last(::Type{LazyAxis{:,P}}) where {P} = known_length(P)
Base.lastindex(x::LazyAxis) = last(x)
Base.last(x::LazyAxis) = _last(known_last(x), x)
_last(::Nothing, x) = last(parent(x))
_last(N::Int, x) = N

known_length(::Type{LazyAxis{N,P}}) where {N,P} = known_size(P, static(N))
known_length(::Type{LazyAxis{:,P}}) where {P} = known_length(P)
@inline function Base.length(x::LazyAxis{N})::Int where {N}
    if known_length(x) === nothing
        return size(getfield(x, :parent), static(N))
    else
        return known_length(x)
    end
end
@inline function Base.length(x::LazyAxis{:})::Int
    if known_length(x) === nothing
        return length(parent(x))
    else
        return known_length(x)
    end
end

Base.axes(x::LazyAxis) = (Base.axes1(x),)
Base.axes1(x::LazyAxis) = x
Base.axes(x::Slice{<:LazyAxis}) = (Base.axes1(x),)
# assuming that lazy loaded params like dynamic length from `size(::Array, dim)` are going
# be used again later with `Slice{LazyAxis}`, we quickly load indices
Base.axes1(x::Slice{<:LazyAxis}) = indices(parent(x.indices))
Base.to_shape(x::LazyAxis) = length(x)

@inline function Base.checkindex(::Type{Bool}, x::LazyAxis, i::Integer)
    if known_first(x) === nothing || known_last(x) === nothing
        return checkindex(Bool, parent(x), i)
    else  # everything is static so we don't have to retrieve the axis
        return (!(known_first(x) > i) || !(known_last(x) < i))
    end
end

@propagate_inbounds function Base.getindex(x::LazyAxis, i::Integer)
    @boundscheck checkindex(Bool, x, i) || throw(BoundsError(x, i))
    return Int(i)
end
@propagate_inbounds function Base.getindex(x::LazyAxis, s::StepRange{<:Integer})
    @boundscheck checkbounds(x, s)
    range(Int(first(x) + s.start-1), step=Int(step(s)), length=Int(length(s)))
end
@propagate_inbounds Base.getindex(x::LazyAxis, i::AbstractUnitRange{<:Integer}) = parent(x)[i]

Base.show(io::IO, x::LazyAxis{N}) where {N} = print(io, "LazyAxis{$N}($(parent(x))))")

"""
    lazy_axes(x)

Produces a tuple of axes where each axis is constructed lazily. If an axis of `x` is already
constructed or it is simply retrieved.
"""
@generated function lazy_axes(x::X) where {X}
    Expr(:block, Expr(:meta, :inline), Expr(:tuple, [:(LazyAxis{$dim}(x)) for dim in 1:ndims(X)]...))
end
lazy_axes(x::LinearIndices) = axes(x)
lazy_axes(x::CartesianIndices) = axes(x)
@inline lazy_axes(x::MatAdjTrans) = reverse(lazy_axes(parent(x)))
@inline lazy_axes(x::VecAdjTrans) = (SOneTo{1}(), first(lazy_axes(parent(x))))
@inline lazy_axes(x::PermutedDimsArray) = permute(lazy_axes(parent(x)), to_parent_dims(x))


text:
module MvNormalWeightedMeanPrecisionTest

using Test
using ReactiveMP
using LinearAlgebra
using Distributions

@testset "MvNormalWeightedMeanPrecision" begin

    @testset "Constructor" begin
        @test MvNormalWeightedMeanPrecision <: AbstractMvNormal

        @test MvNormalWeightedMeanPrecision([ 1.0, 1.0 ]) == MvNormalWeightedMeanPrecision([ 1.0, 1.0 ], [ 1.0, 1.0 ])
        @test MvNormalWeightedMeanPrecision([ 1.0, 2.0 ]) == MvNormalWeightedMeanPrecision([ 1.0, 2.0 ], [ 1.0, 1.0 ])
        @test MvNormalWeightedMeanPrecision([ 1, 2 ])     == MvNormalWeightedMeanPrecision([ 1.0, 2.0 ], [ 1.0, 1.0 ])
        @test MvNormalWeightedMeanPrecision([ 1f0, 2f0 ]) == MvNormalWeightedMeanPrecision([ 1f0, 2f0 ], [ 1f0, 1f0 ])

        @test eltype(MvNormalWeightedMeanPrecision([ 1.0, 1.0 ]))               === Float64
        @test eltype(MvNormalWeightedMeanPrecision([ 1.0, 1.0 ], [ 1.0, 1.0 ])) === Float64
        @test eltype(MvNormalWeightedMeanPrecision([ 1, 1 ]))                   === Float64
        @test eltype(MvNormalWeightedMeanPrecision([ 1, 1 ], [ 1, 1 ]))         === Float64
        @test eltype(MvNormalWeightedMeanPrecision([ 1f0, 1f0 ]))               === Float32
        @test eltype(MvNormalWeightedMeanPrecision([ 1f0, 1f0 ], [ 1f0, 1f0 ])) === Float32
    end

    @testset "distrname" begin
        @test ReactiveMP.distrname(MvNormalWeightedMeanPrecision(zeros(2))) === "MvNormalWeightedMeanPrecision"
    end

    @testset "Stats methods" begin

        xi   = [ -0.2, 5.34, 14.02 ]
        Λ    = [ 1.5 -0.3 0.1; -0.3 1.8 0.0; 0.1 0.0 3.5 ]
        dist = MvNormalWeightedMeanPrecision(xi, Λ)

        @test mean(dist)         ≈  cholinv(Λ) * xi
        @test mode(dist)         ≈  cholinv(Λ) * xi
        @test weightedmean(dist) == xi
        @test invcov(dist)       == Λ
        @test precision(dist)    == Λ
        @test cov(dist)          ≈ cholinv(Λ)
        @test std(dist)          ≈ cholsqrt(cholinv(Λ))
        @test all(mean_cov(dist) .≈ (cholinv(Λ) * xi, cholinv(Λ)))
        @test all(mean_invcov(dist) .≈ (cholinv(Λ) * xi, Λ))
        @test all(mean_precision(dist) .≈ (cholinv(Λ) * xi, Λ))
        @test all(weightedmean_cov(dist) .≈ (xi, cholinv(Λ)))
        @test all(weightedmean_invcov(dist) .≈ (xi, Λ))
        @test all(weightedmean_precision(dist) .≈ (xi, Λ))

        @test length(dist) == 3
        @test entropy(dist) ≈ 3.1517451983126357
        @test pdf(dist, [ 0.2, 3.0, 4.0 ])         ≈ 0.19171503573907536
        @test pdf(dist,  [0.202, 3.002, 4.002])    ≈ 0.19171258180232315
        @test logpdf(dist, [ 0.2, 3.0, 4.0 ])      ≈ -1.6517451983126357
        @test logpdf(dist,  [0.202, 3.002, 4.002]) ≈ -1.6517579983126356

    end

    @testset "Base methods" begin
        @test convert(MvNormalWeightedMeanPrecision{Float32}, MvNormalWeightedMeanPrecision([ 0.0, 0.0 ])) == MvNormalWeightedMeanPrecision([ 0f0, 0f0 ], [ 1f0, 1f0 ])
        @test convert(MvNormalWeightedMeanPrecision{Float64}, [ 0.0, 0.0 ], [ 2 0; 0 3 ]) == MvNormalWeightedMeanPrecision([ 0.0, 0.0 ], [ 2.0 0.0; 0.0 3.0 ])

        @test length(MvNormalWeightedMeanPrecision([ 0.0, 0.0 ]))      === 2
        @test length(MvNormalWeightedMeanPrecision([ 0.0, 0.0, 0.0 ])) === 3
        @test ndims(MvNormalWeightedMeanPrecision([ 0.0, 0.0 ]))       === 2
        @test ndims(MvNormalWeightedMeanPrecision([ 0.0, 0.0, 0.0 ]))  === 3
        @test size(MvNormalWeightedMeanPrecision([ 0.0, 0.0 ]))        === (2, )
        @test size(MvNormalWeightedMeanPrecision([ 0.0, 0.0, 0.0 ]))   === (3, )
    end

    @testset "vague" begin
        @test_throws MethodError vague(MvNormalWeightedMeanPrecision)

        d1 = vague(MvNormalWeightedMeanPrecision, 2)

        @test typeof(d1) <: MvNormalWeightedMeanPrecision
        @test mean(d1)   == zeros(2)
        @test invcov(d1) == Matrix(Diagonal(ReactiveMP.tiny * ones(2)))
        @test ndims(d1) == 2

        d2 = vague(MvNormalWeightedMeanPrecision, 3)

        @test typeof(d2) <: MvNormalWeightedMeanPrecision
        @test mean(d2)   == zeros(3)
        @test invcov(d2) == Matrix(Diagonal(ReactiveMP.tiny * ones(3)))
        @test ndims(d2)  == 3
    end

    @testset "prod" begin
        @test prod(ProdAnalytical(), MvNormalWeightedMeanPrecision([ -1, -1 ], [ 2, 2 ]), MvNormalWeightedMeanPrecision([ 1, 1 ], [ 2, 4 ]))     ≈ MvNormalWeightedMeanPrecision([ 0, 0 ], [ 4, 6 ])

        xi   = [ 0.2, 3.0, 4.0 ]
        Λ    = [ 1.5 -0.1 0.1; -0.1 1.8 0.0; 0.1 0.0 3.5 ]
        dist = MvNormalWeightedMeanPrecision(xi, Λ)

        @test prod(ProdAnalytical(), dist, dist) ≈ MvNormalWeightedMeanPrecision([0.40, 6.00, 8.00], [ 3.00 -0.20 0.20; -0.20 3.60 0.00; 0.20 0.00 7.00])
    end

    @testset "Primitive types conversion" begin
        @test convert(MvNormalWeightedMeanPrecision, zeros(2), Matrix(Diagonal(ones(2)))) == MvNormalWeightedMeanPrecision(zeros(2), Matrix(Diagonal(ones(2))))
        @test begin
            m = rand(5)
            c = Matrix(Symmetric(rand(5, 5)))
            convert(MvNormalWeightedMeanPrecision, m, c) == MvNormalWeightedMeanPrecision(m, c)
        end
    end

end

end


text:
function parents(the_type)
  _parents(the_type, 0)
end

function _parents(the_type, d)
  if(the_type != Any)
    _parents(supertype(the_type), d+1)
  end
  print(the_type)
  if(d > 0)
    print("--->")
  end
end


text:
# this file is executed for building a custom system image of this project

using TimetableSolver

C9 = Division(9, 3)
D10 = Division(10, 4)

ajay = Teacher("Ajay Magotra", "AJA", ["MATH",], [9, 10])
kotwal = Teacher("Kotwal Singh", "KOT", ["MATH",], [9,])
deepika = Teacher("Deepika Ibikunle", "DPK", ["ENG",], [9, 10])
ganguly = Teacher("Ganguly A", "GNG", ["SPORTS",], [9,])
sanjay = Teacher("Sanjay Pandey", "SNJ", ["SCIENCE",], [10,])

subjectcounts_C9 = OrderedDict(
    "MATH"=>2, "ENG"=>2, "SPORTS"=>1
)
subjectcounts_D10 = OrderedDict(
    "MATH"=>2, "ENG"=>1, "SCIENCE"=>2
)

tt_C9 = Timetable(
    [3, 2],
    subjectcounts_C9,
    [ajay, kotwal, deepika, ganguly],
    C9
)
tt_D10 = Timetable(
    [3, 2],
    subjectcounts_D10,
    [ajay, deepika, sanjay],
    D10
)

schedule = Schedule(tt_C9, tt_D10)

vardata = get_data(schedule)
m, modelvars = get_model(vardata)

vardata = get_data(schedule)
model, modelvars = get_model(vardata)
rawsolution, status = get_solution(model, modelvars, vardata)
solution = convertsolution(rawsolution, vardata)
applysolution!(schedule, solution)
solution, status = solve!(schedule)


text:
using BioXMLSciMLTest
using Test

@testset "BioXMLSciMLTest.jl" begin
    # @requires Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
    @testset "cellml" begin include("cellml.jl") end
end


text:
using MolSimReg
using Test

@testset "MolSimReg.jl" begin
    # Write your own tests here.
end


text:
using PyCall
const Chem = PyNULL()
const molecule = PyNULL()
const pydot = PyNULL()
copy!(Chem,pyimport_conda("rdkit.Chem","rdkit","rmg"))
copy!(molecule,pyimport_conda("rmgpy.molecule","rmgpy","rmg"))
copy!(pydot,pyimport_conda("pydot","pydot","rmg"))

include("Constants.jl")
include("Tools.jl")
include("Calculators/RateUncertainty.jl")
include("Calculators/ThermoUncertainty.jl")
include("Calculators/Thermo.jl")
include("Calculators/Diffusion.jl")
include("Calculators/Rate.jl")
include("Calculators/Viscosity.jl")
include("Calculators/kLAkH.jl")
include("Species.jl")
include("Solvent.jl")
include("Reaction.jl")
include("Phase.jl")
include("PhaseState.jl")
include("Interface.jl")
include("Domain.jl")
include("Parse.jl")
include("Reactor.jl")
include("Simulation.jl")
include("TransitorySensitivities.jl")
include("AutomaticMechanismAnalysis.jl")
include("Debugging.jl")
include("EdgeAnalysis.jl")
include("Plotting.jl")
include("fluxdiagrams.jl")


text:
using Conda

Conda.add("pip")
pip = joinpath(Conda.BINDIR, "pip")
run(`$pip install cython`)
run(`$pip install versioneer`)
run(`$pip install git+https://github.com/devitocodes/devito.git`)
run(`$pip install mpi4py`)
run(`$pip install ipyparallel`)

#run(`$pip install devito`)
#run(`$pip install devito[extras]`)


text:
"""
## beam_gm

This subroutine forms the beam geometric matrix for stability analysis.

### Method
```julia
beam_gm(ell::Float64)
```

### Arguments
```julia
* ell::Float64                   : Element length
```

### Return value
```julia
* gm::::Matrix{Float64}(4,4)     : Geometric matrix for beam element
```
"""
function beam_gm(ell::Float64)
  #
  # This subroutine forms the beam geometric matrix for stability analysis.
  #
  # Updated gm
  #
  gm = zeros(4, 4)
  gm[1,1] = 1.2/ell
  gm[1,2] = 0.1
  gm[2,1] = 0.1
  gm[1,3] = -1.2/ell
  gm[3,1] = -1.2/ell
  gm[1,4] = 0.1
  gm[4,1] = 0.1
  gm[2,2] = 2.0*ell/15.0
  gm[2,3] = -0.1
  gm[3,2] = -0.1
  gm[2,4] = -ell/30.0
  gm[4,2] = -ell/30.0
  gm[3,3] = 1.2/ell
  gm[3,4] = -0.1
  gm[4,3] = -0.1
  gm[4,4] = 2.0*ell/15.0
  gm
end


text:
"""
Created on Fri 15 May 2020

@author: Yoann Pradat

Performs NMF rank selection by the method of consensus matrices and related metrics.
"""

#using Pkg
#ENV["Python"] = "/usr/local/anaconda3/bin/python"
#Pkg.build("PyCall")

println("working dir: " *  pwd())

using Distances
using Distributed
using Distributions
using PyCall
using Random

@everywhere include("./src/VNMF.jl")
@everywhere using .VNMF

include(pwd() * "/demo/src/simulation/simulate.jl")

#### folder for saving plots and other files
folder   = "./demo/plot/simulation/rank_consensus_a"
mkpath(folder)

#### # 1. SIMULATE DATA
#### # ################################################################################################################

#### fix rng
rng = MersenneTwister(123)

#### simulate clusters of columns (individuals) and clusters of rows (variables)
W, H, lims_K, lims_N, active_clusters = simulate_WH_clustered(
    F              = 40,
    N              = 200,
    K              = 6,
    n_clusters_N   = 6,
    dist_W         = Uniform(0,1),
    dist_H         = Poisson(500),
    H_cluster_mode = :a,
    rng            = rng
)

#### get col cluster indices
col_indices = zeros(Int, size(H,2))
for i in 1:(length(lims_N)-1)
    range_indices = (lims_N[i]+1):lims_N[i+1]
    col_indices[range_indices] .= i
end

#### simulate noise
E = rand(rng, Poisson(1), size(W,1), size(H, 2))

#### V matrix
V = round.(W * H) + E

py"""
import matplotlib.cm as cm
import numpy as np
import sys
if "." not in sys.path:
    sys.path.append(".")

from demo.src.util.plot_VWH import *

cmap = cm.get_cmap("Paired")

def make_plot_V(V, col_indices, filepath):
    col_indices_colors = {i: cmap(i) for i in np.unique(col_indices)}

    fig = plot_V(V, col_groups=col_indices, col_groups_colors=col_indices_colors)
    fig.savefig(filepath, bbox_inches="tight")
"""

filepath = joinpath(folder, "plot_V_true.pdf")
py"make_plot_V"(V, col_indices, filepath)

#### remove cluster order by shuffling
rand_order_V = randperm(size(V,2))
V_obs = V[:, rand_order_V]
col_indices_obs = col_indices[rand_order_V]

filepath = joinpath(folder, "plot_V_obs.pdf")
py"make_plot_V"(V_obs, col_indices_obs, filepath)

#### # 2. SELECT ALGORITHM
#### # ################################################################################################################

#### params for rank selection
rc_params = RCParams(
    K_min  = 2,
    K_max  = 10,
    n_iter = 50,
    seed   = 123
)

#### nmf algorithm
nmf_global_params = NMFParams(
    init          = :random,
    dist          = Uniform(0, 1),
    max_iter      = 20_000,
    stopping_crit = :conn,
    stopping_iter = 100,
    verbose       = false,
    seed          = 0
)

nmf_local_params = FIParams(
    β            = 2,
    scale_W_iter = false,
    scale_W_last = true,
    alg          = :mm
)

nmf = NMF(
    solver        = nmf_FI,
    global_params = nmf_global_params,
    local_params  = nmf_local_params
)

#### get cophenetic correlation per rank
@time rc_results = rank_by_consensus(V_obs, rc_params, nmf)

#### # 3. PLOT CONSENSUS METRICS
#### # ################################################################################################################

include(pwd() * "/demo/src/util/plot_metrics.jl")

filepath = joinpath(folder, "rank_consensus_metrics.pdf")
plot_metrics_consensus(rc_results.df_metrics, filepath)

#### # 4. PLOT CONSENSUS MATRICES
#### # ################################################################################################################

py"""
from demo.src.util.plot_consensus import *

def make_plot_consensus(cons, filepath):
    config   = ConsensusConfig()
    fig = plot_consensus(cons, config)
    fig.savefig(filepath, bbox_inches='tight')
"""

for (cons, rank) in zip(rc_results.list_cons, rc_results.df_metrics.rank)
    filepath = joinpath(folder, "rank_consensus_$(rank).png")
    py"make_plot_consensus"(cons, filepath)
end

#### # 5. RUN NMF AT BEST RANK
#### # ################################################################################################################

best_rank = rc_results.df_metrics[argmax(rc_results.df_metrics[:,:dispersion]),:rank]

nmf_global_params.rank    = best_rank
nmf_global_params.verbose = true
nmf.global_params = nmf_global_params
nmf.local_params  = nmf_local_params

nmf_res = nmf.solver(V_obs, nmf.global_params, nmf.local_params)

#### # 6. PLOT NMF AT BEST RANK
#### # ################################################################################################################

#### plot W * H with order from consensus at best rank
cons_order = rc_results.list_idxs[rc_results.df_metrics.rank .== best_rank][1]
V_ord = V_obs[:, cons_order]
col_indices_ord = col_indices_obs[cons_order]

filepath = joinpath(folder, "plot_V_ord.pdf")
py"make_plot_V"(V_ord, col_indices_ord, filepath)

#### plot W and and cosine similarity matrix with columns of true W

py"""
from demo.src.util.plot_VWH import *

def make_plot_W_alone(W_fit, W_ref, filepath):
    col_ref_labels = ["ref %d" % i for i in range(W_ref.shape[1])]

    fig = plot_W_alone(
        W_fit          = W_fit,
        W_ref          = W_ref,
        hmap_cmap      = "Blues",
        col_ref_labels = col_ref_labels,
        figsize        = (16,10)

    )
    fig.savefig(filepath, bbox_inches='tight')
"""

filepath = joinpath(folder, "plot_W_alone.pdf")
py"make_plot_W_alone"(nmf_res.W, W, filepath)


text:
using Lexicon, Docile
using WinReg

myfilter(m::Module; files = [""], categories = [:comment, :module, :function, :method, :type, :typealias, :macro, :global]) =
  Lexicon.filter(Docile.Interface.metadata(m), files = files, categories = categories)
myfilter(m::Docile.Interface.Metadata; files = [""], categories = [:comment, :module, :function, :method, :type, :typealias, :macro, :global]) =
  Lexicon.filter(m, files = files, categories = categories)

  #=
# Stuff from Lexicon.jl:
writeobj(any) = string(any)
writeobj(m::Method) = first(split(string(m), "("))
# from base/methodshow.jl
function url(m)
    line, file = m
    try
        d = dirname(file)
        u = Pkg.Git.readchomp(`config remote.origin.url`, dir=d)
        u = match(Pkg.Git.GITHUB_REGEX,u).captures[1]
        root = cd(d) do # dir=d confuses --show-toplevel, apparently
            Pkg.Git.readchomp(`rev-parse --show-toplevel`)
        end
        if beginswith(file, root)
            commit = Pkg.Git.readchomp(`rev-parse HEAD`, dir=d)
            return "https://github.com/$u/tree/$commit/"*file[length(root)+2:end]*"#L$line"
        else
            return Base.fileurl(file)
        end
    catch
        return Base.fileurl(file)
    end
end

function mysave(file::AbstractString, m::Module, order = [:source])
    mysave(file, Docile.Interface.documentation(m), order)
end
function mysave(file::AbstractString, docs::Docile.Interface.Metadata, order = [:source])
    isfile(file) || mkpath(dirname(file))
    open(file, "w") do io
        info("writing documentation to $(file)")
        println(io)
        for (k,v) in EachEntry(docs, order = order)
            name = writeobj(k)
            source = v.data[:source]
            catgory = Docile.Interface.category(v)
            comment = catgory == :comment
            println(io)
            println(io)
            !comment && println(io, "## $name")
            println(io)
            println(io, v.docs.data)
            path = last(split(source[2], r"v[\d\.]+(/|\\)"))
            !comment && println(io, "[$(path):$(source[1])]($(url(source)))")
            println(io)
        end
    end
end
=#

function signature(k::Method, v::Docile.Legacy.Entry)
	n = first(split(string(k), " at "))
	name = first(split(n, "("))
	name = first(split(n, "{"))
	if name == "call"
		t = string(k.sig)
		t = t[7:(end-1)] #delete "Tuple{" and "}"
		t = first(split(t, ","))
		t = t[6:(end-1)] #delete "Type{" and "}"
		comma = search(n, ',')

		return t * (comma == 0 ? "()" : "(" * n[comma+2:end]) #delete "call" and 1st argument
	end

	return n
end
function signature(k::DataType, v::Docile.Legacy.Entry)
	return first(split(string(k), " at "))
end
function signature(k::Function, v::Docile.Legacy.Entry)
	return string(v.data[:signature])
end
function signature(k::Any, v::Docile.Legacy.Entry)
	return string(k)
end

function mysave(file::AbstractString, m::Module, order = [:source])
    mysave(file, Docile.Interface.documentation(m), order)
end
function mysave(file::AbstractString, docs::Docile.Interface.Metadata, order = [:source])
    isfile(file) || mkpath(dirname(file))
    open(file, "w") do io
        info("writing documentation to $(file)")
        println(io)
		entries = [(k, v) for (k,v) in EachEntry(docs, order = order)]
        for (k, v) in entries
			sign = signature(k, v)
            name = first(split(sign, "("))
            source = v.data[:source]
            catgory = Docile.Interface.category(v)
            comment = catgory == :comment
            println(io)
            println(io)
            !comment && println(io, "## $name")
            println(io)
			println(io, "**$sign**")
            println(io)
            println(io, v.docs.data)
            path = last(split(source[2], r"v[\d\.]+(/|\\)"))
            !comment && println(io)
            !comment && println(io, "at $(path):$(source[1])")
            println(io)
        end
    end
end

outdir = dirname(@__FILE__)

#index
save(joinpath(outdir, "api-index.md"), WinReg)


text:
for (op,dotop) in ((:(+),:(.+)), (:(-),:(.-)), (:(*),:(.*)), (:(/),:(./)))
  @eval begin
      function ($op)(x::ArbFloat{P}, y::Array{ArbFloat{P},1}) where {P}
          return ($dotop)(x, y)
      end
      function ($op)(x::Array{ArbFloat{P},1}, y::ArbFloat{P}) where {P}
          return ($dotop)(x, y)
      end
      function ($op)(x::Array{ArbFloat{P},1}, y::Array{ArbFloat{P},1}) where {P}
          return ($dotop)(x, y)
      end
  end
end