Skip to content

Instantly share code, notes, and snippets.

@nazavode
Last active September 25, 2019 21:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nazavode/edb8f6fa790d59d95ddacb281dae8087 to your computer and use it in GitHub Desktop.
Save nazavode/edb8f6fa790d59d95ddacb281dae8087 to your computer and use it in GitHub Desktop.
Generate C++ periodic table of elements
#!/usr/bin/env python3
###########################################################
# This is free and unencumbered software released into the public domain.
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
# For more information, please refer to <http://unlicense.org>
###########################################################
UPSTREAM = "https://gist.github.com/nazavode/edb8f6fa790d59d95ddacb281dae8087"
HELP = (
"""
Generate a periodic table of elements header source for C++.
Meant to use source data in json format as specified here:
https://github.com/Bowserinator/Periodic-Table-JSON
In order to generate a proper table without a json input file
readily available, just download the reference one and pipe
it into tablegen:
$ curl https://raw.githubusercontent.com/Bowserinator/Periodic-Table-JSON/master/PeriodicTableJSON.json | tablegen
Please find upstream here:
"""
+ UPSTREAM
)
import json
import datetime
import string
import argparse
FIELDS = (
"number",
"period",
"atomic_mass",
"density",
"boil",
"melt",
"electron_affinity",
"name",
"symbol",
"electron_configuration",
)
TEMPLATE = r"""
<%setopt("strip", False)%>
// Generated by tablegen:
// {{UPSTREAM}}
#ifndef TABLEGEN_ELEMENTS_H_INCLUDED
#define TABLEGEN_ELEMENTS_H_INCLUDED
#include <string_view>
#include <optional>
#include <array>
namespace elements {
struct element_t {
int number;
int period;
double atomic_mass;
std::optional<double> density;
std::optional<double> boil;
std::optional<double> melt;
std::optional<double> electron_affinity;
std::string_view name;
std::string_view symbol;
std::string_view electron_configuration;
};
using namespace std::string_view_literals;
inline constexpr std::array<element_t, <%echo(len(elements))%>> table = {{
%for element in elements:
{
%for field in fields:
%if isinstance(element[field], str):
"{{element[field]}}"sv, // {{field}}
%elif element[field] is None:
std::nullopt, // {{field}}
%else:
{{element[field]}}, // {{field}}
%endif
%endfor
},
%endfor
}};
enum class Symbol {
%for element in elements:
{{element['symbol']}} = {{element['number']}}, // {{element['name']}}
%endfor
};
} // namespace elements
#endif // TABLEGEN_ELEMENTS_H_INCLUDED
"""
###########################################################
# 'step' tiny template engine
# Taken from https://github.com/dotpy/step
# No way we are going to use Jinja here
import re
import sys
PY3 = False
if sys.version_info > (3, 0):
PY3 = True
class Template(object):
COMPILED_TEMPLATES = {} # {template string: code object, }
# Regex for stripping all leading, trailing and interleaving whitespace.
RE_STRIP = re.compile(
"(^[ \t]+|[ \t]+$|(?<=[ \t])[ \t]+|\A[\r\n]+|[ \t\r\n]+\Z)", re.M
)
def __init__(self, template, strip=True):
"""Initialize class"""
super(Template, self).__init__()
self.template = template
self.options = {"strip": strip}
self.builtins = {
"escape": lambda s: escape_html(s),
"setopt": lambda k, v: self.options.update({k: v}),
}
if template in Template.COMPILED_TEMPLATES:
self.code = Template.COMPILED_TEMPLATES[template]
else:
self.code = self._process(self._preprocess(self.template))
Template.COMPILED_TEMPLATES[template] = self.code
def expand(self, namespace={}, **kw):
"""Return the expanded template string"""
output = []
namespace.update(kw, **self.builtins)
namespace["echo"] = lambda s: output.append(s)
namespace["isdef"] = lambda v: v in namespace
eval(compile(self.code, "<string>", "exec"), namespace)
return self._postprocess("".join(map(to_unicode, output)))
def stream(self, buffer, namespace={}, encoding="utf-8", **kw):
"""Expand the template and stream it to a file-like buffer."""
def write_buffer(s, flush=False, cache=[""]):
# Cache output as a single string and write to buffer.
cache[0] += to_unicode(s)
if flush and cache[0] or len(cache[0]) > 65536:
buffer.write(postprocess(cache[0]))
cache[0] = ""
namespace.update(kw, **self.builtins)
namespace["echo"] = write_buffer
namespace["isdef"] = lambda v: v in namespace
postprocess = lambda s: s.encode(encoding)
if self.options["strip"]:
postprocess = lambda s: Template.RE_STRIP.sub("", s).encode(encoding)
eval(compile(self.code, "<string>", "exec"), namespace)
write_buffer("", flush=True) # Flush any last cached bytes
def _preprocess(self, template):
"""Modify template string before code conversion"""
# Replace inline ('%') blocks for easier parsing
o = re.compile("(?m)^[ \t]*%((if|for|while|try).+:)")
c = re.compile("(?m)^[ \t]*%(((else|elif|except|finally).*:)|(end\w+))")
template = c.sub(r"<%:\g<1>%>", o.sub(r"<%\g<1>%>", template))
# Replace ({{x}}) variables with '<%echo(x)%>'
v = re.compile("\{\{(.*?)\}\}")
template = v.sub(r"<%echo(\g<1>)%>\n", template)
return template
def _process(self, template):
"""Return the code generated from the template string"""
code_blk = re.compile(r"<%(.*?)%>\n?", re.DOTALL)
indent = 0
code = []
for n, blk in enumerate(code_blk.split(template)):
# Replace '<\%' and '%\>' escapes
blk = re.sub(r"<\\%", "<%", re.sub(r"%\\>", "%>", blk))
# Unescape '%{}' characters
blk = re.sub(r"\\(%|{|})", "\g<1>", blk)
if not (n % 2):
# Escape backslash characters
blk = re.sub(r"\\", r"\\\\", blk)
# Escape double-quote characters
blk = re.sub(r'"', r'\\"', blk)
blk = (" " * (indent * 4)) + 'echo("""{0}""")'.format(blk)
else:
blk = blk.rstrip()
if blk.lstrip().startswith(":"):
if not indent:
err = "unexpected block ending"
raise SyntaxError("Line {0}: {1}".format(n, err))
indent -= 1
if blk.startswith(":end"):
continue
blk = blk.lstrip()[1:]
blk = re.sub("(?m)^", " " * (indent * 4), blk)
if blk.endswith(":"):
indent += 1
code.append(blk)
if indent:
err = "Reached EOF before closing block"
raise EOFError("Line {0}: {1}".format(n, err))
return "\n".join(code)
def _postprocess(self, output):
"""Modify output string after variables and code evaluation"""
if self.options["strip"]:
output = Template.RE_STRIP.sub("", output)
return output
def escape_html(x):
"""Escape HTML special characters &<> and quotes "'."""
CHARS, ENTITIES = "&<>\"'", ["&amp;", "&lt;", "&gt;", "&quot;", "&#39;"]
string = x if isinstance(x, basestring) else str(x)
for c, e in zip(CHARS, ENTITIES):
string = string.replace(c, e)
return string
def to_unicode(x, encoding="utf-8"):
"""Convert anything to Unicode."""
if PY3:
return str(x)
if not isinstance(x, unicode):
x = unicode(str(x), encoding, errors="replace")
return x
###########################################################
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description=HELP, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"infile",
nargs="?",
default="-",
metavar="INFILE",
type=argparse.FileType("r"),
help="input json periodic table, defaults to '-' (standard input)",
)
args = parser.parse_args()
parsed = json.loads(args.infile.read())
namespace = {"UPSTREAM": UPSTREAM, "elements": parsed["elements"], "fields": FIELDS}
print(Template(TEMPLATE).expand(namespace))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment