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

//@version=5

// @description Functions for working with timeframes. The timeframeToMinutes() function returns the number of minutes in an arbitrary timeframe string.
library("TimeframeToMinutes", overlay=true)

string inputTF = input.string(defval="D", title="Example Timeframe")


// @function timeframeToMinutes() returns the number of minutes in the supplied timeframe string, which is arbitrary, i.e. it doesn't have to be the timeframe of the current chart but can be taken from an input.
// The sole advantage over the short and neat Pinecoders f_resInMinutes function from their excellent MTF Selection Framework (at https://www.tradingview.com/script/90mqACUV-MTF-Selection-Framework-PineCoders-FAQ/) is that it doesn't use up a security() call.
// To convert the other way, from minutes to timeframe.period format, I would use the f_resFromMinutes function from the Pinecoders' MTF Selection Framework, which does not use security().
// Error-checking:: It has light error-checking to make sure the string is in the format timeframe.period, e.g. 15S, 1 (minute), 60 (1H), 1D, 1W, 1M.
// It will throw an error for some non-standard timeframes such as 30 hours (1800 minutes). Above 1440 minutes, only whole numbers of days are allowed. This is to be consistent with the security() function.
// But it will allow some non-standard timeframes such as 7 hours (420 minutes). Such timeframes must still be supplied in the standard timeframe.period format.
// @param _tf The timeframe to convert to minutes. Must be in timeframe.period format.
// @returns A float representing the number of minutes that the timeframe period is equivalent to.

var string errorTextLibraryName = "SimpleCryptoLife/TimeframeToMinutes"

export timeframeToMinutes(string _tf) =>
    var string _errorTextFunctionName = "timeframeToMinutes()"
    if _tf == ""
        runtime.error("Timeframe cannot be empty. Using input.timeframe() gives an empty string if it is set to Chart. 
          You need to convert an empty timeframe to the current chart timeframe before passing it to this library.
          An example is given in the library of how to do this.
          [Error001" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    _arrayTFString = str.split(_tf, "")  // Split string into an array
    bool _tfContainsS = array.includes(_arrayTFString, "S")// Check if it contains S because the error-checking is different for seconds
    int _lastIndexTFString = array.size(_arrayTFString) - 1 // Get the index of the last element in the array (indexes are numberd 0,1,2 etc)
    // Pop each array element (remove from the end of the array) and check if it's an integer.
    _arrayTFInt = array.new_int(0) // Create a new array to hold the integers
    _minuteMultiplier = 1  // Initialise the minute multiplier to one in case there's no D, W, or M letter.
    for i = 0 to _lastIndexTFString
        _elementTFString = array.pop(_arrayTFString)
        // If it is an integer, convert to an integer and push it (add it to the end) into another array of type int. This should reverse the number.
        bool _isInt = not na(str.tonumber(_elementTFString))
        if _isInt
            _float = str.tonumber(_elementTFString)
            _int = int(_float)
            array.push(_arrayTFInt,_int)
        // If it is not an integer and is one of "D", "W", "M", convert it to an integer:  D = 1440m, W = 10080, M = 43800. This minute multiplier is initialised as 1.
        _minuteMultiplier := switch _elementTFString
            "S" => 10  // Just to get past error-checking
            "D" => 1440
            "W" => 10080
            "M" => 43800
            => _minuteMultiplier
        if _minuteMultiplier == 1 and not _isInt and not na(_elementTFString) and not _tfContainsS
            runtime.error("Timeframe is badly formed. [Error002" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")// If it is not null, but not any of the above, throw an error because the timeframe is badly formed.
    // If the integer array has more than 5 elements, throw an error because that's higher than 1 day and so the timeframe is badly formed.
    if array.size(_arrayTFInt) > 4
        runtime.error("Timeframe is badly formed. [Error003" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    // If the integer array is empty and the minute multiplier is 1, throw an error because the timeframe is badly formed.
    bool _intArrayIsEmpty = array.sum(_arrayTFInt) == 0 or na(array.sum(_arrayTFInt))
    if _minuteMultiplier == 1 and _intArrayIsEmpty and not _tfContainsS
        runtime.error("Timeframe is badly formed. [Error004" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    // If the integer array is empty and the minute multiplier is > 1, populate the integer array with 1.
    if _intArrayIsEmpty and _minuteMultiplier > 1
        array.push(_arrayTFInt,1)
    // Pop each element of the integer array, multiply it by 10 to the power of the element index, and push it to another array.
    int _lastIndexTFInt = array.size(_arrayTFInt) - 1 // Get the index of the last element in the integer array
    _arrayTFToSum = array.new_int(0) // Create a new array to hold the integers
    for i = 0 to _lastIndexTFInt
        int _elementTFInt = array.shift(_arrayTFInt)
        _elementTFIntPow = _elementTFInt * math.pow(10,i)
        int _elementTFIntPowInt = int(_elementTFIntPow)  // Have to cast it to int because the previous operation makes it a float
        array.push(_arrayTFToSum,_elementTFIntPowInt)
    int _sumIntTF = array.sum(_arrayTFToSum)  // Sum the array.
    // If the sum of minutes is greater than 1440, throw an error because that is greater than one day.
    if _sumIntTF > 1440
        runtime.error("Timeframe is greater than 1D and must be specified in a whole number of days [Error005" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    // Multiply the sum by the minute multiplier to get the final number of minutes.
    float _finalMinutes = _sumIntTF * _minuteMultiplier
    // Hack override if it's one of the four seconds intervals supported by TradingView
    _finalMinutes := switch _tf
        "S" => 0.0166666667
        "5S" => 0.0833333333
        "15S" => 0.25
        "30S" => 0.5
        => _finalMinutes
    if _tfContainsS and (_tf != "S" and _tf != "5S" and _tf != "15S" and _tf != "30S")
        runtime.error("Timeframe is a disallowed number of seconds. [Error006" + " from function " + _errorTextFunctionName + " in library " + errorTextLibraryName + "]")
    _finalMinutes


// DEMONSTRATION
// string inputTFFixed = inputTF == "" ? timeframe.period : inputTimeframe  // Uncomment to fix empty timeframes before sending to the function
var numberOfMinutes = timeframeToMinutes(inputTF)  // You only need to call this function once, to avoid unnecessary calculations, hence assigning the results using var.
import SimpleCryptoLife/Labels/1 as SCL_Label
string debugText = "Chart timeframe: " + str.tostring(timeframe.period) + "\nInput timeframe: " + str.tostring(inputTF) + "\nNo. of minutes: " + str.tostring(numberOfMinutes)
SCL_Label.labelLast(_text=debugText, _offset=3)
