# Python algorithm that converts array-like data into MathJax

I frequently use MathJax, and often I need to write matrices. Since writing matrices using MathJax is very tiresome task, I decided to automate it.

Code:

import numpy as np
import pyperclip

class Conversion():

def __init__(self, matrix, n = None, m = None):
if matrix == None:
self.matrix = np.random.randint(-10,10, size = [n,m])
else:
'''If variable "matrix", for example, contains [1,2,3,4], instead of [[1,2,3,4]], functions below
will throw error, hence we need to make sure that the data type is correct.'''
if type(matrix[0]) in (str,int,float):
self.matrix = np.array([matrix,])
else:
self.matrix = np.array(matrix)

self.rows = len(self.matrix)
self.cols = len(self.matrix[0])

def single(self, choosen_brackets = ')'):
available_brackets = {' ' : 'matrix',
')' : 'pmatrix',
']' : 'bmatrix',
'}' : 'Bmatrix',
'|' : 'vmatrix',
'||': 'Vmatrix'  }
choosen_brackets = available_brackets[choosen_brackets]
body = '$$\\begin {} \n'.format('{' + choosen_brackets + '}') for row in self.matrix: row = [str(x) for x in row] body += ' & '.join(row) + '\\\\' + '\n' body +='\end {}$$'.format('{' + choosen_brackets + '}')
print(body)
pyperclip.copy(body)

def augmented(self, choosen_brackets = '[]'):
'''We are assuming that the last column of the given matrix is a vector.'''

pos_of_the_verticar_bar = '{' + 'c'*(self.cols-1) + '|' + 'c' + '}'
body = '$$\\left [ \\begin {array} %s \n' % (pos_of_the_verticar_bar) for row in self.matrix: row = [str(x) for x in row] body += ' & '.join(row) + '\\\\' + '\n' body +='\end {array} \\right ]$$'
print(body)
pyperclip.copy(body)


Notes:

1. Since function augmented is quite similar to single, I could've merged them into one. However, I think that keeping them separate makes the code a little bit more readable.

2. On MSE, mathjax equations are enclosed with  instead of \\$

3. "single" is a bad name for a function, I admit. I haven't found any better options. Feel free to offer suggestions.

What can be improved?

Written for Python 3.6.5.

This looks like a handy tool to have around, maybe we can make it even better.

# Style

As per the official Python Style Guide (often known by his nickname PEP8), keyword-arguments of functions should have no whitespace around the =.

# Matrix size

numpy arrays have a property called .shape which describes their number of rows and number of cols (in case of a 2D array at least). So

self.rows = len(self.matrix)
self.cols = len(self.matrix[0])


could become

self.rows, self.cols = self.matrix.shape


Since your code only works for 2D matrices, it's maybe also a good idea to check for that. The .ndim attribute can be of help here.

# Type checking

type(matrix[0]) in (str,int,float) should instead be isinstance(matrix[0], (str, int, float)).

# Checking None

The official recommendation is to always use if sth is None: when checking for None.

# String joining

row = [str(x) for x in row]
body += ' & '.join(row) + '\\\\' + '\n'


could be done in a single line:

body += ' & '.join(str(x) for x in row) + '\\\\' + '\n'


This means you also don't have to reassign row to be something different than it was before.

# String formatting

Since you said you're using Python 3.6, maybe have a look at f-strings for even more concise string formatting syntax. No matter if you choose to do this, maybe at least get rid of the old-style % formatting in augmented(...) by escaping { and } using a leading \.

# Function output

I find it preferable if you'd let the user decide what he wants to do with what your function returns. So instead of printing and copying the formatted matrix code, maybe just return body and let the caller decide how to proceed from here. You could even define a "convenience" function (to_clipboard) that basically only does pyperclip.copy(...), although this is likely not necessary. Another idea would be to make at least the copy part opt-in via a "flag" bool parameter.

# Class

Maybe a class is not the right tool here. A possible alternative would be to move what's done in __init__ into a "private" (read name starts with _) helper function and get rid of the class.

# Missing backslash

There seems to be a little bug in both of your functions (see how bad code duplication is ;-)): body +='\end ... is likely missing an additional \.

# Unused argument

If I'm not mistaken, the choosen_brackets argument of augmented is not used in the function.

# Typo

choosen_brackets should likely be chosen_brackets

• Thank you for your review! May I ask why it is recommended to use is None instead of == None? Commented Oct 16, 2019 at 2:19
• Strictly speaking, is checks if both variables point to the exact same object in memory and not just that they compare equal. Since None is a singleton, this is likely the more natural operation to use. But to be honest, there are probably not a lot of (other) hard facts why you should use is None over == None. Both variants will give you the desired result here. Commented Oct 16, 2019 at 7:47

Python's string formatting has come a long way from the "%s" formatting days. Nowadays classes can even determine on their own how to handle format specifiers. Therefore I would write a matrix class that can be pretty printed with different format options determining the matrix style.

class MathJaxMatrix:
brackets = {'': 'matrix',
'p': 'pmatrix',
'b': 'bmatrix',
'B': 'Bmatrix',
'v': 'vmatrix',
'V': 'Vmatrix'}
e_msg = "unsupported format string passed to MathJaxMatrix.__format__"

def __init__(self, m):
if m.ndim == 1:
m = m.reshape(len(m), 1)
self.m = m
self.rows, self.cols = m.shape

def __str__(self):
return "\\\\ \n".join(" & ".join(str(x) for x in row) for row in self.m)

def __format__(self, format_spec=None):
if format_spec is None:
return str(self)
if format_spec == "a":
format_spec = '{' + 'c'*(self.cols-1) + '|c}'
start = rf'$$\left[ \begin{{array}}{format_spec}' end = r'\end{array} \right]$$'
else:
try:
brackets = self.brackets[format_spec]
except KeyError as e:
raise TypeError(self.e_msg) from e
start = f'$$\\begin{{{brackets}}}' end = f'\end{{{brackets}}}$$'
return "\n".join([start, str(self), end])


Which you can use like this:

In [40]: x = np.random.rand(4, 5)

In [41]: m = MathJaxMatrix(x)

In [42]: print(m)
0.35170079706 & 0.903087473471 & 0.748996998207 & 0.741200595894 & 0.771233795397\\
0.251204439922 & 0.40876741255 & 0.101668325527 & 0.738733484611 & 0.3052742949\\
0.448079803976 & 0.273533142438 & 0.368031240997 & 0.34312026244 & 0.587809084934\\
0.0192109217812 & 0.334069285732 & 0.644616319752 & 0.648226279564 & 0.307678962448

In [43]: print(f"{m}")
$$\begin{matrix} 0.35170079706 & 0.903087473471 & 0.748996998207 & 0.741200595894 & 0.771233795397\\ 0.251204439922 & 0.40876741255 & 0.101668325527 & 0.738733484611 & 0.3052742949\\ 0.448079803976 & 0.273533142438 & 0.368031240997 & 0.34312026244 & 0.587809084934\\ 0.0192109217812 & 0.334069285732 & 0.644616319752 & 0.648226279564 & 0.307678962448 \end{matrix}$$

In [44]: print(f"{m:p}")
$$\begin{pmatrix} 0.35170079706 & 0.903087473471 & 0.748996998207 & 0.741200595894 & 0.771233795397\\ 0.251204439922 & 0.40876741255 & 0.101668325527 & 0.738733484611 & 0.3052742949\\ 0.448079803976 & 0.273533142438 & 0.368031240997 & 0.34312026244 & 0.587809084934\\ 0.0192109217812 & 0.334069285732 & 0.644616319752 & 0.648226279564 & 0.307678962448 \end{pmatrix}$$

In [45]: print(f"{m:a}")
$$\left[ \begin{array}{cccc|c} 0.35170079706 & 0.903087473471 & 0.748996998207 & 0.741200595894 & 0.771233795397\\ 0.251204439922 & 0.40876741255 & 0.101668325527 & 0.738733484611 & 0.3052742949\\ 0.448079803976 & 0.273533142438 & 0.368031240997 & 0.34312026244 & 0.587809084934\\ 0.0192109217812 & 0.334069285732 & 0.644616319752 & 0.648226279564 & 0.307678962448 \end{array} \right]$$

In [62]: print(f"{m:e}")
---------------------------------------------------------------------------
KeyError                                 Traceback (most recent call last)
...
KeyError: 'e'

The above exception was the direct cause of the following exception:

TypeError                                Traceback (most recent call last)
...
TypeError: unsupported format string passed to MathJaxMatrix.__format__


Note that this removes the repeated code for getting a string representation of the actual matrix, uses np.ndarray.ndim and np.reshape to ensure it is a proper 2D matrix. I used the first letter of the different *matrix options to distinguish them because } is not allowed in format specifications.

The actual convenience function is then quite short:

def format_matrix(m, format_spec):
m = MathJaxMatrix(m)
s = f"{m:{format_spec}}"
print(s)
pyperclip.copy(s)