Tangling Multiple Org Files

I've been looking off and on for ways to combine separate code-blocks in org-mode into a single tangled file. I wanted to use it because I tangle code that I want to re-use out of posts but then if I want to break the posts up I need to create a separate file (tangle) for each post. I'm hopeful that this method will allow me to break up a tangle across multiple posts. I've only tried it on toy files but I want to get some initial documentation for it in place.

The Steps

Let's say that there are two source org-files:

  • one.org: contains the tangle block and a source block
  • two.org: contains another block that we want to tangle with the one in one.org

The steps are:

  1. Put an #+INCLUDE directive to include two.org into one.org
  2. Export one.org to an org file
  3. Open the exported org file (one.org.org)
  4. Tangle it.

Create one.org

The file one.org is going to have the tangle and the first source-block:

#+begin_src python :tangle ~/test.py :exports none
<<block-one>>

<<block-two>>
#+end_src
#+begin_src python :noweb-ref block-one
def one():
    print("One")
#+end_src

We also need to include what's in the second file (two.org). The code we want to include is in a section called Two so we can include just that section by adding a search term at the end.

#+INCLUDE: "./two.org::*Two"

Create two.org

In the other file add the section header to match the INCLUDE search term (*Two) and put a code block with a reference named block-two to match what's in the tangle block above.

* Two
#+begin_src python :noweb-ref block-two
def two():
print("Two")
#+end_src

Export one.org

Tangling unfortunately ignores the INCLUDE directive so we have to export it first to another org-file in order to get the text from org.two into our source file. By default, exporting to org is disabled so you need to enable it (e.g. starting with M-x customize org-export-backends).

Once it's enabled you can export one.org to an org-mode file using C-c C-e O v (the default name will be one.org.org).

Tangle one.org.org

The last choice when we exported the file in the previous step (v) will save it to a file and open it up in an emacs buffer. When the buffer is open you can then tangle it (C-c C-v C-t) and the output (/test.py from our tangle block) should contain both of our functions.

Sources

This is where I got the information on breaking up the files. It includes some emacs-lisp to run the steps automatically (although I didn't try it):

This is the post that mentions that exporting org-files to org-format needs to be enabled (and how to do it):

This is the manual page explaining the search syntax (which is what the #+INCLUDE format uses).

This explains the #+INCLUDE directive options:

CodeWars: Pick Peaks

Table of Contents

Beginning

The problem given is to write a function that returns the location and values of local maxima within a list (array). The inputs will be (possibly empty) lists with integers. The first and last elements cannot be called peaks since we don't know what comes before the first element or after the last element.

Code

Imports

# pypi
from expects import equal, expect

The Submission

def pick_peaks(array: list) -> dict:
    """Find local maxima

    Args:
     array: list of integers to search

    Returns:
     pos, peaks dict of maxima
    """
    output = dict(pos=[], peaks=[])
    peak = position = None

    for index in range(1, len(array)):
        if array[index - 1] < array[index]:
            position = index
            peak = array[index]
        elif peak is not None and array[index - 1] > array[index]:
            output["pos"].append(position)
            output["peaks"].append(peak)
            peak = position = None
    return output
expect(pick_peaks([1,2,3,6,4,1,2,3,2,1])).to(equal({"pos":[3,7], "peaks":[6,3]}))
expect(pick_peaks([3,2,3,6,4,1,2,3,2,1,2,3])).to(equal({"pos":[3,7], "peaks":[6,3]}))
expect(pick_peaks([3,2,3,6,4,1,2,3,2,1,2,2,2,1])).to(equal({"pos":[3,7,10], "peaks":[6,3,2]}))
expect(pick_peaks([2,1,3,1,2,2,2,2,1])).to(equal({"pos":[2,4], "peaks":[3,2]}))
expect(pick_peaks([2,1,3,1,2,2,2,2])).to(equal({"pos":[2], "peaks":[3]}))
expect(pick_peaks([2,1,3,2,2,2,2,5,6])).to(equal({"pos":[2], "peaks":[3]}))
expect(pick_peaks([2,1,3,2,2,2,2,1])).to(equal({"pos":[2], "peaks":[3]}))
expect(pick_peaks([1,2,5,4,3,2,3,6,4,1,2,3,3,4,5,3,2,1,2,3,5,5,4,3])).to(equal({"pos":[2,7,14,20], "peaks":[5,6,5,5]}))
expect(pick_peaks([18, 18, 10, -3, -4, 15, 15, -1, 13, 17, 11, 4, 18, -4, 19, 4, 18, 10, -4, 8, 13, 9, 16, 18, 6, 7])).to(equal({'pos': [5, 9, 12, 14, 16, 20, 23], 'peaks': [15, 17, 18, 19, 18, 13, 18]}))
expect(pick_peaks([])).to(equal({"pos":[],"peaks":[]}))
expect(pick_peaks([1,1,1,1])).to(equal({"pos":[],"peaks":[]}))

CodeWars: Simple Pig Latin

Description

Move the first letter of each word to the end of it, then add "ay" to the end of the word. Leave punctuation marks untouched.

Code

Imports

# pypi
from expects import equal, expect

Submission

import re

LETTERS = re.compile(r"[a-zA-Z]")
WORD_BOUNDARY = re.compile(r"\b")


def convert(token: str) -> str:
    """Convert a single word to pig-latin

    Args:
     token: string representing a single token

    Returns: 
     pig-latinized word (if appropriate)
    """
    return (f"{token[1:]}{token[0]}ay"
            if token and LETTERS.match(token) else token)


def pig_it(text: str) -> str:
    """Basic pig latin converter

    Moves first letter of words to the end and adds 'ay' to the end

    Args:
     text: string to pig-latinize

    Returns:
     pig-latin version of text
    """
    return "".join(convert(token) for token in WORD_BOUNDARY.split(text))
expect(pig_it('Pig latin is cool')).to(equal('igPay atinlay siay oolcay'))
expect(pig_it('This is my string')).to(equal('hisTay siay ymay tringsay'))
expect(pig_it("Hello World !")).to(equal("elloHay orldWay !"))

CodeWars: RGB To Hexadecimal

Description

Given three arguments r, g, and b which are integers from 0 to 255 representing RGB values, convert them to a six digit (zero-padded) hexadecimal string. Invalid (out of range) values need to be rounded to nearest value.

The Code

Imports

# pypi
from expects import equal, expect

Submission

HEX_DIGITS = "0123456789ABCDEF"
RGB_TO_HEX = dict(zip(range(len(HEX_DIGITS)), HEX_DIGITS))
HEX = 16

def rgb(r: int, g: int, b: int) -> str:
    """Convert RGB to Hexadecimal

    If the values are out of bounds they will be set to the nearest limit

    e.g. -1 becomes 0 and 2919 becomes 255

    Non-integers will raise an error

    Args:
     r: red channel (0-255)
     g: green channel (0-255)
     b: blue channel (0-255)

    Returns:
     6-digit hexadecimal equivalent of r, g, b
    """
    colors = (max(min(color, 255), 0) for color in (r, g, b))
    converted = ((RGB_TO_HEX[color//HEX], RGB_TO_HEX[color % HEX])
                 for color in colors)
    return "".join((y for x in converted for y in x))
expect(rgb(0,0,0)).to(equal("000000"))
expect(rgb(1,2,3)).to(equal("010203"))
expect(rgb(255,255,255)).to(equal("FFFFFF"))
expect(rgb(254,253,252)).to(equal("FEFDFC"))
expect(rgb(-20,275,125)).to(equal("00FF7D"))

Alternatives

A surprising number of people used the string formatting - {:02X} to convert the numbers to hexadecimal. I think that's sort of the problem with these earlier puzzles - there's a big question of how much of python's built in functionality you should use. I guess since I use string formatting a lot that might make sense as a shortcut in this case.

def rgb_2(r: int, g: int, b: int) -> str:
    """Convert RGB to Hexadecimal

    If the values are out of bounds they will be set to the nearest limit

    e.g. -1 becomes 0 and 2919 becomes 255

    Non-integers will raise an error

    Args:
     r: red channel (0-255)
     g: green channel (0-255)
     b: blue channel (0-255)

    Returns:
     6-digit hexadecimal equivalent of r, g, b
    """
    return "".join((f"{max(min(color, 255), 0):02X}" for color in (r, g, b)))
expect(rgb_2(0,0,0)).to(equal("000000"))
expect(rgb_2(1,2,3)).to(equal("010203"))
expect(rgb_2(255,255,255)).to(equal("FFFFFF"))
expect(rgb_2(254,253,252)).to(equal("FEFDFC"))
expect(rgb_2(-20,275,125)).to(equal("00FF7D"))

Not quite so readable, but short.

CodeWars: Rot13

Description

Given a string, replace each letter with the one that comes 13 letters after it in the alphabet. Ignore non-English aphabetical characters.

The Code

Imports

# python
from string import ascii_lowercase as lowercase
from string import ascii_uppercase as uppercase

# pypi
from expects import equal, expect

The Submitted Function

This is the version I submitted to CodeWars. It uses the dict update method to build a dict (although the solutions below seem neater) and the get method to handle the cases where the letter in the message isn't in the dictionary.

def rot13(message: str) -> str:
    """Implement a Caesar Cipher by shifting letters 13 places

    Non-english letters are left as-is

    Args:
     message: string to encode

    Return:
     the encoded version of the input string
    """
    code = {letter: lowercase[(index + 13) % 26] 
             for index, letter in enumerate(lowercase)}
    code.update((letter, uppercase[(index + 13) % 26])
                  for index, letter in enumerate(uppercase))
    return "".join(code.get(letter, letter) for letter in message)

A Test

def tester(encoder):
    inputs = ("test", "Test", "Test5")
    expecteds = ("grfg", "Grfg", "Grfg5")

    for message, expected in zip(inputs, expecteds):
        encoded = encoder(message)
        expect(encoded).to(equal(expected))
        expect(encoder(encoded)).to(equal(message))
    return

tester(rot13)

Alternatives

Quite a few of the other solutions (on the first page, anyway) used the built in str.maketrans and str.translate methods (they complement each other). I didn't see anything in the documentation about how defaults are handled so I'd have to look into it more. The top answer also used slicing instead of modulo (lower[13:] + lower[:13]) which might be better. The comments mention that the top answer actually won't work anymore since the maketrans and translate functions got moved out of string (which is where it's importing it from).

The top solutions seem to have a mix of current python and deprecated python (python 2?) so you'd have to be careful in using any of them.

Using the Slicing

If you were use slicing instead of the modulo I think it might look like this (from here on out I'm going to declare the code-books outside the function the way I would normally do it, I kind of didn't really think about it when submitting the solution above so I'll leave it as is).

CODE = dict(zip(lowercase + uppercase,
                lowercase[13:] + lowercase[:13] +
                uppercase[13:] + uppercase[:13]))

def rot13_2(message: str) -> str:
    """Implement a Caesar Cipher by shifting letters 13 places

    Non-english letters are left as-is

    Args:
     message: string to encode

    Return:
     the encoded version of the input string
    """
    return "".join(CODE.get(letter, letter) for letter in message)
tester(rot13_2)

This is more compact, although I'm not sure that the slicing is as immediately obvious as the use of the modulo is.

Using translate and maketrans

Here's a version using the built-in maketrans and translate functions.

CODE = str.maketrans(lowercase + uppercase,
                     lowercase[13:] + lowercase[:13] +
                     uppercase[13:] + uppercase[:13])


def rot13_3(message: str) -> str:
    """Implement a Caesar Cipher by shifting letters 13 places

    Non-english letters are left as-is

    Args:
     message: string to encode

    Return:
     the encoded version of the input string
    """
    return message.translate(CODE)
tester(rot13_3)
coded = rot13_3("I have been to paradise 3 times, but I have never been to me. "
                "Oh, the humanity!")
print(coded)
print(rot13_3(coded))
V unir orra gb cnenqvfr 3 gvzrf, ohg V unir arire orra gb zr. Bu, gur uhznavgl!
I have been to paradise 3 times, but I have never been to me. Oh, the humanity!

It kind of seems too much to use translate for this exercise, but it does feel cleaner than the dictionary, so I'll have to keep it in mind for the future.