lib_array
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © robbatt

// version history

//@version=5
//@description several array functions for chained calls, batch conversion, incrementing and comparing arrays.
library("lib_array", overlay = true)

///////////////////////
//      UTIL
///////////////////////

//@description sort an array and return it (compared to the default function returning void)
//@param id The array to sort (and return again)
//@param descending The sort order: order.ascending (default:false, meaning omit this param and just call myArray.sort()) or order.descending => set descending=true
//@return The array that was passed as parameter id
export method sort(int[] id, bool descending = false) =>
    array.sort(id, descending ? order.descending : order.ascending)
    id

//@description sort an array and return it (compared to the default function returning void)
//@param id The array to sort (and return again)
//@param descending The sort order: order.ascending (default:false, meaning omit this param and just call myArray.sort()) or order.descending => set descending=true
//@return The array that was passed as parameter id
export method sort(float[] id, bool descending = false) =>
    array.sort(id, descending ? order.descending : order.ascending)
    id

//@description sort an array and return it (compared to the default function returning void)
//@param id The array to sort (and return again)
//@param descending The sort order: order.ascending (default:false, meaning omit this param and just call myArray.sort()) or order.descending => set descending=true
//@return The array that was passed as parameter id
export method sort(string[] id, bool descending = false) =>
    array.sort(id, descending ? order.descending : order.ascending)
    id

// IN / DECREMENT

_increment(id, by_value) =>
    len = id.size()
    for [idx, i] in id
        id.set(idx, i + by_value)    
    id

//@description increment all values in an array by the given value
//@param id The array to increment (and return again)
//@param by_value The value by which to increment (default: 1)
//@return The array that was passed as parameter id
export method increment(int[] id, int by_value = 1) =>
    _increment(id, by_value)

//@description increment all values in an array by the given value
//@param id The array to increment (and return again)
//@param by_value The value by which to increment (default: 1)
//@return The array that was passed as parameter id
export method decrement(int[] id, int by_value = 1) =>
    id.increment(-by_value)

//@description increment all values in an array by the given value
//@param id The array to increment (and return again)
//@param by_value The value by which to increment (default: 1.0)
//@return The array that was passed as parameter id
export method increment(float[] id, float by_value = 1.0) =>
    _increment(id, by_value)

//@description increment all values in an array by the given value
//@param id The array to increment (and return again)
//@param by_value The value by which to increment (default: 1.0)
//@return The array that was passed as parameter id
export method decrement(float[] id, float by_value = 1) =>
    id.increment(-by_value)



//////////////////////////
//      CONVERSION
//////////////////////////


//@description convert an array of strings that each represent a number into a new array of integers. If the values are not properly formed integers, they will be converted to na
//@param id The array to convert
export method toint(string[] id) =>
    int_array = array.new<int>()
    for s in id
        int_array.push(int(str.tonumber(s)))
    int_array

//@description convert an array of floats into a new array of integers.
//@param id The array to convert
export method toint(float[] id) =>
    int_array = array.new<int>()
    for f in id
        int_array.push(int(f))
    int_array

//@description convert an array of strings that each represent a number into a new array of integers. If the values are not properly formed integers or floating point values, they will be converted to na
//@param id The array to convert
export method tofloat(string[] id) =>
    float_array = array.new<float>()
    for s in id
        float_array.push(str.tonumber(s))
    float_array


//@description convert an array of integers into a new array of floats. 
//@param id The array to convert
export method tofloat(int[] id) =>
    float_array = array.new<float>()
    for i in id
        float_array.push(float(i))
    float_array

//@description convert an array of integers into a new array of strings. If the values are na, they will be converted to the string "NaN".
//@param id The array to convert
export method tostring(int[] id) =>
    str_array = array.new<string>()
    for i in id
        str_array.push(str.tostring(i))
    str_array

//@description convert an array of integers into a new array of strings. If the values are na, they will be converted to the string "NaN".
//@param id The array to convert
export method tostring(float[] id) =>
    str_array = array.new<string>()
    for i in id
        str_array.push(str.tostring(i))
    str_array

//@description convert an array of floats into a new array of booleans.
//@param id The array to convert
export method tobool(float[] id) =>
    bool_array = array.new<bool>()
    for f in id
        bool_array.push(bool(f))
    bool_array

//@description convert an array of floats into a new array of booleans.
//@param id The array to convert
export method tobool(int[] id) =>
    bool_array = array.new<bool>()
    for i in id
        bool_array.push(bool(i))
    bool_array

//@description convert an array of strings into a new array of booleans. A string is true if it is not na AND longer than 0
//@param id The array to convert
export method tobool(string[] id) =>
    bool_array = array.new<bool>()
    for i in id
        bool_array.push(na(i) ? false : str.length(i) > 0)
    bool_array


//@description convert an array of booleans to 0/1 and count all the 1s
//@param id The array to convert
export method sum(bool[] id) =>
    sum = 0
    for b in id
        sum += b ? 1 : 0


//////////////////////////
//      QUEUES
//////////////////////////

// TODO revise when generics are added to pine script, so we can pass custom types as well
_enqueue(id, item, int max, bool condition = true, bool lifo = false) =>
    if not na(item) and condition
        if lifo // Last In First Out
            array.push(id,item)
            if id.size() > max
                id.shift()
        else // First In Last Out
            array.unshift(id,item)
            if id.size() > max
                id.pop()
    id

//@description enqueue a value into an array, if the resulting length of the array exceeds max, the oldest inserted element is removed
//@param id The array that is used as queue
//@param item The item to enqueue (at pos 0, unless lifo = true)
//@param max The max size of the queue
//@param condition An optional flag that allows disabling the adding, which in turn will prevent for in loops from ever running and save performance where not needed
//@param lifo An optional flag that allows to change the behavior from First In Last Out (default and consistent with pine scripts history operator with most recent elements at index 0) to a more common and resource efficient approach in programming languages: Last In First Out 
//@returns The queue passed as param id
export method enqueue(int[] id, int item, int max, bool condition = true, bool lifo = false) =>
    _enqueue (id,item, max, condition, lifo)
//@description enqueue a value into an array, if the resulting length of the array exceeds max, the oldest inserted element is removed
//@param id The array that is used as queue
//@param item The item to enqueue (at pos 0, unless lifo = true)
//@param max The max size of the queue
//@param condition An optional flag that allows disabling the adding, which in turn will prevent for in loops from ever running and save performance where not needed
//@param lifo An optional flag that allows to change the behavior from First In Last Out (default and consistent with pine scripts history operator with most recent elements at index 0) to a more common and resource efficient approach in programming languages: Last In First Out 
//@returns The queue passed as param id
export method enqueue(float[] id, float item, int max, bool condition = true, bool lifo = false) =>
    _enqueue (id,item, max, condition, lifo)
//@description enqueue a value into an array, if the resulting length of the array exceeds max, the oldest inserted element is removed
//@param id The array that is used as queue
//@param item The item to enqueue (at pos 0, unless lifo = true)
//@param max The max size of the queue
//@param condition An optional flag that allows disabling the adding, which in turn will prevent for in loops from ever running and save performance where not needed
//@param lifo An optional flag that allows to change the behavior from First In Last Out (default and consistent with pine scripts history operator with most recent elements at index 0) to a more common and resource efficient approach in programming languages: Last In First Out 
//@returns The queue passed as param id
export method enqueue(string[] id, string item, int max, bool condition = true, bool lifo = false) =>
    _enqueue (id,item, max, condition, lifo)
//@description enqueue a value into an array, if the resulting length of the array exceeds max, the oldest inserted element is removed
//@param id The array that is used as queue
//@param item The item to enqueue (at pos 0, unless lifo = true)
//@param max The max size of the queue
//@param condition An optional flag that allows disabling the adding, which in turn will prevent for in loops from ever running and save performance where not needed
//@param lifo An optional flag that allows to change the behavior from First In Last Out (default and consistent with pine scripts history operator with most recent elements at index 0) to a more common and resource efficient approach in programming languages: Last In First Out 
//@returns The queue passed as param id
export method enqueue(line[] id, line item, int max, bool condition = true, bool lifo = false) =>
    _enqueue (id,item, max, condition, lifo)
//@description enqueue a value into an array, if the resulting length of the array exceeds max, the oldest inserted element is removed
//@param id The array that is used as queue
//@param item The item to enqueue (at pos 0, unless lifo = true)
//@param max The max size of the queue
//@param condition An optional flag that allows disabling the adding, which in turn will prevent for in loops from ever running and save performance where not needed
//@param lifo An optional flag that allows to change the behavior from First In Last Out (default and consistent with pine scripts history operator with most recent elements at index 0) to a more common and resource efficient approach in programming languages: Last In First Out 
//@returns The queue passed as param id
export method enqueue(box[] id, box item, int max, bool condition = true, bool lifo = false) =>
    _enqueue (id,item, max, condition, lifo)


///////////////////////
//      TEST
///////////////////////
// import robbatt/lib_log/2 as LOG
// import robbatt/lib_unit/3 as UNIT

// var UNIT.Test assert = UNIT.Test.new(false, true)
// assert.init()

// // test setup
// array<string> initial = array.from('99', '100')
// assert.equals(initial.get(0), '99', 'first item of initial array should be 99')
// assert.equals(initial.get(1), '100', 'second item of initial array should be 100')

// // check sorting and enqueueing works
// array<string> sorted_str = initial.sort()
// assert.equals(sorted_str.get(0), '100', 'first item of sorted string array should be "100" (because 1<9)')
// assert.equals(sorted_str.get(1), '99', 'second item of sorted string array should be "99"')


// // check conversion works from strings to ints
// array<int> converted_int = sorted_str.toint()
// assert.equals(converted_int.get(0), 100, 'first item of sorted string array, converted to integers, should be 100')
// assert.equals(converted_int.get(1), 99, 'second item of sorted string array, converted to integers, should be 99')

// // check sorting and enqueueing works
// array<int> sorted_int = converted_int.sort()
// assert.equals(sorted_int.get(0), 99, 'first item of sorted integer array should be 99')
// assert.equals(sorted_int.get(1), 100, 'second item of sorted integer array should be 100')

// // check calculations on converted numbers  
// array<int> incremented = sorted_int.increment()
// assert.equals(incremented.get(0), 100, 'first item of incremented integer array should be 100')
// assert.equals(incremented.get(1), 101, 'second item of incremented integer array should be 101')

// // check enqueueing works
// array<int> enqueued_int = incremented.enqueue(99, 2).enqueue(100, 2)
// assert.equals(enqueued_int.size(), 2, 'length of integer queue should be 2')
// assert.equals(enqueued_int.get(0), 100, 'first item of integer queue should be 100')
// assert.equals(enqueued_int.get(1), 99, 'second item of integer queue should be 99')

// array<string> enqueued_str = array.from('center', 'extra').enqueue('filo', 2).enqueue('lifo', 3, lifo = true)
// assert.equals(enqueued_str.size(), 3, 'length of string queue should be 3')
// assert.equals(enqueued_str.get(0), 'filo', 'first item of string queue should be "filo"')
// assert.equals(enqueued_str.get(1), 'center', 'second item of string queue should be "center"')
// assert.equals(enqueued_str.get(2), 'lifo', 'second item of string queue should be "lifo"')
if barstate.isfirst
    label.new(bar_index, close, 'minimum required plot')