def find_range(arr: list[int], val: int) -> list[int]:
    """Binary search to find the range of a given number ‘val’.

    The range of val will be the first and last position of val in the arr.

    Complexity:
        Time: Θ(logn) <=> O(logn) & Ω(logn) (since we must consider duplicates)
        Space: O(1)

    Args:
        arr: array of numbers sorted in ascending order
        val: element in arr for which to find the inclusive range

    Returns: the inclusive range of val idxs if val is present, else [-1, -1]

    Examples:
        >>> find_range([4, 6, 6, 6, 9], 6)
        [1, 3]
        >>> find_range([1, 3, 8, 10, 15], 10)
        [3, 3]
        >>> find_range([1, 3, 8, 10, 15], 12)
        [-1, -1]
        >>> find_range([], 12)
        [-1, -1]

    """

    def binary_search(find_last_pos: bool) -> int:
        check_left = lambda: val < arr[mid] or val == arr[mid] and not find_last_pos
        check_right = lambda: val > arr[mid] or val == arr[mid] and find_last_pos
        ## INITIALIZE VARS ##
        val_idx = -1
        start, end = 0, len(arr) - 1

        ## BINARY SEARCH ##
        while start <= end:
            mid = (start + end) // 2
            # Found a val idx
            if val == arr[mid]:
                val_idx = mid

            if check_left():
                end = mid - 1
            elif check_right():  # pragma: no branch
                start = mid + 1

        # first or last pos of val_idx if val exists, -1 otherwise
        return val_idx

    ## EDGE CASES ##
    if not arr:  # Redundant given binary search conditions
        return [-1, -1]

    """ALGORITHM"""

    first_pos = binary_search(find_last_pos=False)
    # Minor optimization for better constants (logn vs. 2logn);
    #   if 'val' not present in `arr`, skip `last_pos` search
    last_pos = -1 if first_pos == -1 else binary_search(find_last_pos=True)
    return [first_pos, last_pos]