<jupyter_start><jupyter_text>Indexes> to_filename("Something Interesting", index="2.b.13")Produces a filename like:> 2.b.13.something.interesting.mdIf I want to work with indexes directly, I can:> next_index('1.a')> '1.b'> next_index('1.a.2')> '1.a.3'> next_index('1.abc', alphabet=list('abc')> '1.aca'<jupyter_code>#hide
from nbdev.showdoc import *
#export
from slip_box.imports import *<jupyter_output><empty_output><jupyter_text>Use the AlphabetA deep index can get confusing. 1.1.1.1.1.2 makes it easy to forget or add a 1. Instead, I use 1.a.1.a.1.2. I can still make a mistake, but I make fewer mistakes.This is the principle, and I'll swear by it until my last breath: do one thing. Do one thing at a time. Don't try and remember things, do math, think about the note I'm creating, think about whether it will be useful for something, or whatever. The better I am at focusing, the more effective my work.<jupyter_code>#export

alphabet = list('abcdefghijklmnopqrstuvwxyz')

def integer_to_alpha(n, alphabet=alphabet):
    """Convert numbers > 0 to alphabetic representations.
    For the regular alphabet, 1 == a, 26==z, 27==aa,
    52 == az, etc."""
    string = ''
    base = len(alphabet)
    
    while n > 0:
        denominator = n // base # denominator from floor division
        remainder = n % base

        # Take the nth letter in the alphabet.
        # 5 would be e, which is the 5th position in the alphabet
        # with an index of 4 (zero-based index).
        # If there is no remainder, 0 - 1 is -1, the last
        # letter in the alphabet.
        string = f'{alphabet[remainder - 1]}{string}'

        if remainder == 0: # no remainder means decrement the denominator...
            denominator -= 1
        n = denominator
    return string

def letter_to_value(letter, alphabet=alphabet):
    """Where does this letter fall in this alphabet?"""
    if not letter in alphabet: return 0
    return (alphabet.index(letter) + 1)

def alpha_to_integer(string, alphabet=alphabet):
    """Convert a string to an integer"""
    letters = list(str(string)) # 'abc' -> ['a', 'b', 'c']
    base = len(alphabet)
    increasing = list(reversed(letters))
    result = 0
    for position, letter in enumerate(increasing):
        value = letter_to_value(letter, alphabet=alphabet)
        value_at_position = (base ** position) * value
        result += value_at_position
    return result
short = list('abc')

assert integer_to_alpha(1, short) == 'a'
assert integer_to_alpha(2, short) == 'b'
assert integer_to_alpha(3, short) == 'c'
assert integer_to_alpha(4, short) == 'aa'
assert integer_to_alpha(7, short) == 'ba'

assert letter_to_value('a', short) == 1
assert letter_to_value('b', short) == 2
assert letter_to_value('c', short) == 3
assert letter_to_value('d', short) == 0
assert letter_to_value('A', short) == 0

# From 1 to a large number, if I convert the integer
# to an alpha, then convert it back, it matches.
for i in range(1, 1000):
    alpha = integer_to_alpha(i, short)
    result = alpha_to_integer(alpha, short)
    assert i == result<jupyter_output><empty_output><jupyter_text>Making SenseSo, combining alphabets, but starting at 1 is useful for indexing. I'm not feeling confident that 1 == a is the place to start. Number systems usually find zero and negative numbers useful. This is a practical system, for indexing, and I'm not sure I know what 0 index in the sense of finding a note means, and I'm sure I don't need to do that.Also, I like that I'm using the approach that <NAME> (1927-1998) orginally used. He had notecards in a box, I have notes in a directory. We both use this sort of indexing.  Increment IndexesI index so I know where to stick the next slip.1 becomes 2, 1.a becomes 1.b when we're just appending the next slip to the tree. This is next_index.If I'm adding a slip that qualifies, challenges, or clarifies an idea, I often like to make it a child. So, a slip about the age of the Sphynx might be about the standard Egyptologists saying it was first constructed around 2,500 BC. If I wanted to use notes from <NAME>, the geologist, I'd add a child note that he claims they were built before 9,700 BC. So, the first note could be 3.c.2 and the second 3.c.2.a.<jupyter_code>#export

def is_integer(o):
    try:
        return int(o) == float(o)
    except:
        return False

def validate_index(current, separator='.', **kw):
    """Ensure the index can be iterated."""
    if current is None: return False
    if current == '': return False
    if len(str(current).split(separator)) == 0: return False
    return True
        
def next_index(current, separator='.', default='1', alphabet=alphabet, **kw):
    """Get the next value for an index.
    None returns '1', '' returns '1',
    1.a returns 1.b, etc."""
    
    if not validate_index(current, separator=separator): return default
    
    current = str(current)
    parts = current.split(separator)
    o = parts[-1]
    valid = parts[:-1]
    
    if is_integer(o):
        integer = int(o)
        if integer < 1: integer = 0
        value = str(integer + 1)
    else:
        integer = alpha_to_integer(o, alphabet=alphabet)
        value = integer_to_alpha(integer + 1, alphabet=alphabet)

    valid.append(value)

    return separator.join(valid)

def next_child(current, separator='.', default='1', alphabet=alphabet, **kw):
    """Get the next child for an index.
    None returns '1', '' returns '1',
    1.a returns 1.a.1, 2 returns 2.a."""
    
    if not validate_index(current, separator=separator): return default

    current = str(current)
    o = current.split(separator)[-1]
    if is_integer(o):
        value = alphabet[0]
    else:
        value = '1'
    
    return current + separator + value
assert next_index(None) == '1'
assert next_index(None, default="foo") == "foo"
assert next_index('-1') == '1'
assert next_index('') == '1'
assert next_index('1') == '2'
assert next_index(41) == '42'
assert next_index('1.a') == '1.b'
assert next_index('42.zsa.13') == '42.zsa.14'
assert next_index('2|b|3', separator='|') == '2|b|4'

assert next_child(None) == '1'
assert next_child(None, default="foo") == "foo"
assert next_child('-1') == '-1.a'
assert next_child('') == '1'
assert next_child(41) == '41.a'
assert next_child(41, alphabet=list('bcd')) == '41.b'
assert next_child('1.a') == '1.a.1'
assert next_child('42.zsa') == '42.zsa.1'
assert next_child('2|b|3', separator='|') == '2|b|3|a'
assert next_index('1.abc', alphabet=list('abc')) == '1.aca'<jupyter_output><empty_output><jupyter_text>Making SenseSo, next_index and next_child are working as I'd expect. This version of the code is a **LOT** cleaner than the version I'm replacing. The other system used 20-30 functions and there were bugs in there I didn't have the patience to find.I like the idea that most of these functions take a fairly robust set of parameters, so I can use a different alphabet or separator.  FilenamesFor simplicity, I'm replacing shell whitespace characters with underscores, by default, but not dealing with other problematic input. I may just white list filenames later.A default filename for "As Good As It Gets" attaching after 1.a would be 1.b.as_good_as_it_gets.md.<jupyter_code>#export

def to_filename(s, reference=None, index=None, child=False, index_separator=".", title_separator="_", suffix="md", **kw):
    s = str(s).casefold()
    title = title_separator.join(s.split())
    if index is None:
        fn = next_child if child else next_index
        index = fn(reference, separator=index_separator, **kw)
    return f"{index}.{title}.{suffix}"
assert to_filename('abc 123') == '1.abc_123.md'
assert to_filename('  ABC   123   ') == '1.abc_123.md'
assert to_filename('abc 123', title_separator='|') == '1.abc|123.md'
assert to_filename('abc 123', index='2.b.1') == '2.b.1.abc_123.md'
assert to_filename('abc 123', reference='2.b') == '2.c.abc_123.md'
assert to_filename('abc 123', reference='2.b', child=True) == '2.b.1.abc_123.md'
assert to_filename('abc 123', suffix='markdown') == '1.abc_123.markdown'<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12639267145333002, 'fraction_numerical': 0.027605843030453082, 'mean_word_length': 3.8406231276213303, 'pattern_counts': {'":': 0, '<': 20, '<?xml version=': 0, '>': 30, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 1, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '152064', 'license': 'Apache-2.0', 'n_tokens_mistral': 2748, 'n_tokens_neox': 2550, 'n_words': 926, 'path': 'nbs/00.00.indexes.ipynb', 'repo_name': 'davidrichards/slip_box', 'size': '8093'}