diff --git a/po/laview-latex-struct-0.pot b/po/laview-latex-struct-0.pot index 6504219..44b4dcd 100644 --- a/po/laview-latex-struct-0.pot +++ b/po/laview-latex-struct-0.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: laview-latex-struct-0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-07-18 18:10+0400\n" +"POT-Creation-Date: 2014-07-31 18:16+0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -41,6 +41,18 @@ msgstr "" msgid "Incorrect longtable parameters doesn't match '%s' regexp." msgstr "" +#: /home/kolan/projects/LAview/LaTeX-Struct/src/Table.vala:298 +msgid "2nd param (ATable) isn't a child of the 1st (Glob)." +msgstr "" + +#: /home/kolan/projects/LAview/LaTeX-Struct/src/Table.vala:312 +msgid "3rd param (limits) is incorrect. Read the manual." +msgstr "" + +#: /home/kolan/projects/LAview/LaTeX-Struct/src/Table.vala:329 +msgid "Cann't split the table. Read the manual." +msgstr "" + #: /home/kolan/projects/LAview/LaTeX-Struct/src/TableParser.vala:413 #, c-format msgid "Unexpected end external tag sequence '%s' without begin tag pair." diff --git a/po/ru/laview-latex-struct-0.po b/po/ru/laview-latex-struct-0.po index 0b48ef4..dfa2a2f 100644 --- a/po/ru/laview-latex-struct-0.po +++ b/po/ru/laview-latex-struct-0.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: laview-latex-struct-0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-07-18 18:10+0400\n" +"POT-Creation-Date: 2014-07-31 18:16+0400\n" "PO-Revision-Date: 2014-05-28 10:50+0400\n" "Last-Translator: \n" "Language-Team: Russian\n" @@ -44,6 +44,18 @@ msgstr "Ошибка разбора поддокумента." msgid "Incorrect longtable parameters doesn't match '%s' regexp." msgstr "Неверные параметры longtable не удовлетворяют рег. выражению '%s'." +#: /home/kolan/projects/LAview/LaTeX-Struct/src/Table.vala:298 +msgid "2nd param (ATable) isn't a child of the 1st (Glob)." +msgstr "2-ой параметр (ATable) не является дочерним узлом 1-ого (Glob)." + +#: /home/kolan/projects/LAview/LaTeX-Struct/src/Table.vala:312 +msgid "3rd param (limits) is incorrect. Read the manual." +msgstr "3-ий параметр (limits) не верный. Смотрите документацию." + +#: /home/kolan/projects/LAview/LaTeX-Struct/src/Table.vala:329 +msgid "Cann't split the table. Read the manual." +msgstr "Невозможно разбить таблицу. Смотрите документацию." + #: /home/kolan/projects/LAview/LaTeX-Struct/src/TableParser.vala:413 #, c-format msgid "Unexpected end external tag sequence '%s' without begin tag pair." diff --git a/src/Table.vala b/src/Table.vala index 1fad106..366a6cd 100644 --- a/src/Table.vala +++ b/src/Table.vala @@ -5,6 +5,27 @@ namespace LAview { */ namespace Table { + /** + * Any error at ``ATable`` splitting. + */ + public errordomain SplitError { + + /** + * ``ATable`` isn't a child of the {@link Glob}. + */ + ISNT_CHILD, + + /** + * Any errors in the split indexes. + */ + INDEX_ERROR, + + /** + * Any other error. + */ + OTHER, + } + /** * Any Table in the LaTeX document. */ @@ -58,7 +79,7 @@ namespace LAview { * Gets a copy of the ``ATable``. */ public override IDoc copy () { - var clone = Object.new (this.get_type ()) as ATable; + var clone = Object.new (get_type ()) as ATable; clone.align = align; clone.style = style; @@ -176,6 +197,139 @@ namespace LAview { last_footer.clone_col (src_index, dest_index, multicol, line_style); table.clone_col (src_index, dest_index, multicol, line_style); } + + /** + * Bounds of the ``ATable`` to split. + */ + public struct SplitLimit { + + /** + * First column index [0; last]. + */ + uint first; + + /** + * Last column index [first; ncols - 1]. + */ + uint last; + + /** + * Maximum of columns per page [1; ncols]. + */ + uint max_cols; + } + + bool check_limits (Array sorted_limits) { + /* check nearby limits */ + for (var i = 1; i < sorted_limits.length; ++i) + if (sorted_limits.index (i - 1).last >= sorted_limits.index (i).first + || sorted_limits.index (i).first > sorted_limits.index (i).last) + return false; + + /* check limits of the first and last elements */ + if (sorted_limits.index (0).first > sorted_limits.index (0).last + || params.size <= sorted_limits.index (sorted_limits.length - 1).last) + return false; + + return true; + } + + uint [] get_indexes (Array sorted_limits) { + var lim_indexes = new uint[sorted_limits.length]; + for (var i = 0; i < sorted_limits.length; ++i) + lim_indexes[i] = sorted_limits.index (i).first; + return lim_indexes; + } + + ATable? split_table (Array sorted_limits, uint [] lim_indexes, + Row.OpLineStyle line_style) { + + var return_table = copy () as ATable; + bool split_finish = true; + + /* removing spare columns */ + for (uint i = sorted_limits.length - 1; i < sorted_limits.length; --i) { // group + for (uint j = sorted_limits.index (i).last; + j >= lim_indexes[i] + sorted_limits.index (i).max_cols + && j <= sorted_limits.index (i).last; --j) + return_table.remove_col ((int)j, line_style); + + for (uint j = lim_indexes[i] - 1; j >= sorted_limits.index (i).first && j < lim_indexes[i]; --j) + return_table.remove_col ((int)j, line_style); + + /* count indexes */ + if (lim_indexes[i] <= sorted_limits.index (i).last) { + split_finish = false; + lim_indexes[i] += sorted_limits.index (i).max_cols; + } + } + + /* did any indexes updated */ + if (split_finish) + return null; + + return return_table; + } + + /** + * Split an ``ATable`` into several ``ATable``s by columns according to the limits. + * + * For example: table<
> + * ``[fix1 fix2 colA1 colA2 colA3 colA4 colA5 fix3 fix4 colB1 colB2 colB3 colB4 fix5 fix6]``<
> + * with limits { {2, 6, 2}, {9, 12, 3} }<
> + * will be splitted into 3 tables<
> + * [fix1 fix2 colA1 colA2 fix3 fix4 colB1 colB2 colB3 fix5 fix6]<
> + * [fix1 fix2 colA3 colA4 fix3 fix4 colB4 fix6]<
> + * [fix1 fix2 colA5 fix3 fix4 fix6]<
> + * 3rd param 'limits'. For all elements following conditions should be satisfied. + * last[i] < first[i+1], 0 <= first[i] <= last[i] <= ncols-1, 1 <= max_cols <= ncols. + * + * @param glob {@link Glob} document with a ``ATable``. + * @param limits array of {@link SplitLimit}s. + * @param line_style {@link Row.OpLineStyle} of the operation. + * + * @return number of ``ATable``s the table splitted to. + */ + public uint split (Glob glob, Array limits, + Row.OpLineStyle line_style = Row.OpLineStyle.BORDER_DBLLINES) throws SplitError { + /* is table a child of glob */ + var glob_index = glob.index_of (this); + if (glob_index == -1) + throw new SplitError.ISNT_CHILD (_("2nd param (ATable) isn't a child of the 1st (Glob).")); + + /* sorting limits */ + var sorted_limits = new Array.sized (false, false, sizeof (SplitLimit), limits.length); + sorted_limits.append_vals (limits.data, limits.length); + + sorted_limits.sort ((ref a, ref b) => { + if (a.first < b.first) return -1; + if (a.first > b.first) return 1; + return 0; + }); + + /* checking limits for intersections */ + if (!check_limits (sorted_limits)) + throw new SplitError.INDEX_ERROR (_("3rd param (limits) is incorrect. Read the manual.")); + + /* split the table on several longtables inserting them before glob_index + 1 */ + var lim_indexes = get_indexes (sorted_limits); + + ATable temp_table; + uint result = 0; + var part_idx = glob_index + 1; + while (null != (temp_table = split_table (sorted_limits, lim_indexes, line_style))) { + glob.insert (part_idx++, temp_table); + ++result; + } + + /* remove table from the doc */ + if (result != 0) + glob.remove_at (glob_index); + else + throw new SplitError.OTHER (_("Cann't split the table. Read the manual.")); + + return result; + } } } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0ba40bc..7b1cdb3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -33,6 +33,18 @@ VALA_PRECOMPILE (LTableTest_C ${LTableTestSources} ADD_EXECUTABLE (LTableTest ${LTableTest_C}) TARGET_LINK_LIBRARIES (LTableTest ${PROJ_LCASE} ${GEE_LIBRARIES}) +# add split_test executable +FILE (GLOB_RECURSE LTableTestSources RELATIVE ${CMAKE_SOURCE_DIR}/test SplitTest.vala) +VALA_PRECOMPILE (SplitTest_C ${LTableTestSources} + PACKAGES gee-0.8 posix + OPTIONS --thread ${VALA_DEBUG} + CUSTOM_VAPIS ${PROJECT_BINARY_DIR}/src/${PROJ_LCASE}-${MAJOR}.vapi + GENERATE_VAPI + GENERATE_HEADER + ) +ADD_EXECUTABLE (SplitTest ${SplitTest_C}) +TARGET_LINK_LIBRARIES (SplitTest ${PROJ_LCASE} ${GEE_LIBRARIES}) + # parsing test macro MACRO (do_parse_test testname table_path etalon_path regexp) IF ("${etalon_path}" STREQUAL "") @@ -386,5 +398,19 @@ ltable_test (clone_1000_0 ${PROJECT_SOURCE_DIR}/test/tex/formular.tex ${PROJECT_ ltable_test (append_row0 ${PROJECT_SOURCE_DIR}/test/tex/table_rows.tex ${PROJECT_SOURCE_DIR}/test/tex/table_rows.etalon.tex append_row0 "Etalon and generated text are EQUAL .-.") +MACRO (do_split_test testname limits table etalon regexp) + ADD_TEST (split_test-${testname} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/SplitTest + --limits ${limits} --table ${table} --etalon ${etalon}) +SET_TESTS_PROPERTIES (split_test-${testname} + PROPERTIES PASS_REGULAR_EXPRESSION ${regexp} + FAIL_REGULAR_EXPRESSION "CRITICAL;WARNING") +ENDMACRO (do_split_test) + +# test TeXReport_splitLongtable () function +do_split_test (split_test ${PROJECT_SOURCE_DIR}/test/tex/limits1.in + ${PROJECT_SOURCE_DIR}/test/tex/limits_table1.tex + ${PROJECT_SOURCE_DIR}/test/tex/limits_table1.etalon.tex +"Etalon and generated text are EQUAL ...\n") + # enable testing ENABLE_TESTING () diff --git a/test/SplitTest.vala b/test/SplitTest.vala new file mode 100644 index 0000000..bc46218 --- /dev/null +++ b/test/SplitTest.vala @@ -0,0 +1,137 @@ +using LAview; + +public class Main : Object { + static string fname_limits = ""; + static string fname_table = ""; + static string fname_etalon = ""; + static string fname_write = ""; + + const OptionEntry [] options = { + { "limits", 'l', 0, OptionArg.FILENAME, ref fname_limits, "File with limits", null }, + { "table", 't', 0, OptionArg.FILENAME, ref fname_table, "File with a table", null }, + { "etalon", 'e', 0, OptionArg.FILENAME, ref fname_etalon, "File with etalon table", null }, + { "write", 'w', 0, OptionArg.FILENAME, ref fname_write, "File to write", null }, + { null } + }; + + public static int main (string [] args) { + + Intl.setlocale (LocaleCategory.ALL, ""); + + /* commandline arguments processing */ + try { + var opt_context = new OptionContext ("- tests LaTeX parser"); + opt_context.set_help_enabled (true); + opt_context.add_main_entries (options, null); + opt_context.parse (ref args); + } catch (OptionError e) { + stderr.printf ("error: %s\n", e.message); + stderr.printf ("Run '%s --help' to see a full list of available command line options.\n", args[0]); + return -1; + } + + /* read limits */ + if (fname_limits == null) { + stderr.printf ("Specify file with limits\n"); + return -1; + } + + var stream = FileStream.open (fname_limits, "r"); + if (stream == null) { + stdout.puts ("Cann't open limits file\n"); + return -1; + } + + uint lim[3] = { 0, 0, 0}; + var limits = new Array (); + + while (3 == stream.scanf ("%u %u %u", out lim[0], out lim[1], out lim[2])) { + var split_lim = Table.ATable.SplitLimit (); + split_lim.first = lim[0]; + split_lim.last = lim[1]; + split_lim.max_cols = lim[2]; + limits.append_val (split_lim); + } + + /* read table */ + if (fname_table == null) { + stderr.printf ("Specify file with a table or read help (%s --help)", args[0]); + return -1; + } + + /* load file contents */ + string contents; + try { + FileUtils.get_contents (fname_table, out contents); + } catch (FileError e) { + stderr.printf ("error: %s\n", e.message); + return -1; + } + + /* parse TeX */ + Glob doc; + try { + doc = LAview.parse (contents); + stdout.printf ("TeX document successfully parsed\n"); + + } catch (Parsers.ParseError e) { + stderr.printf ("Error parsing TeX document: %s\n", e.message); + return -1; + } + + /* find a longtable object */ + Table.Longtable table = null; + foreach (var subdoc in doc) { + if (subdoc is Table.Longtable) { + table = subdoc as Table.Longtable; + break; + } + } + + if (table == null) { + stderr.puts ("longtable object not found\n"); + return -1; + } + + /* split the table */ + try { + table.split (doc, limits); + } catch (Table.SplitError e) { + stderr.puts (e.message); + return -1; + } + + /* load etalon file */ + if (fname_etalon != null) { + try { + FileUtils.get_contents (fname_etalon, out contents); + } catch (FileError e) { + stderr.printf ("error: %s\n", e.message); + return -1; + } + } + + /* generate */ + var generated = doc.generate (); + + /* compare with an etalon */ + if (contents == generated) + stdout.puts ("Etalon and generated text are EQUAL ;-)\n"); + else + stdout.puts ("Etalon and generated text are NOT EQUAL ;-(\n"); + + stdout.printf ("--- Generated plain-TeX (generated) ---\n%s", generated); + + /* write to file */ + if (fname_write != null ) + try { + FileUtils.set_contents (fname_write, generated); + } catch (FileError e) { + stderr.printf ("error: %s\n", e.message); + return -1; + } + + return 0; + } + +} diff --git a/test/tex/limits1.in b/test/tex/limits1.in new file mode 100644 index 0000000..9f17b03 --- /dev/null +++ b/test/tex/limits1.in @@ -0,0 +1,3 @@ +9 11 3 +2 6 2 +13 16 1 diff --git a/test/tex/limits_table1.etalon.tex b/test/tex/limits_table1.etalon.tex new file mode 100644 index 0000000..4b19a19 --- /dev/null +++ b/test/tex/limits_table1.etalon.tex @@ -0,0 +1,127 @@ +%% LyX 2.0.3 created this file. For more info, see http://www.lyx.org/. +%% Do not edit unless you really know what you are doing. +\documentclass[english]{article} +\usepackage[T1]{fontenc} +\usepackage[latin9]{inputenc} +\usepackage{longtable} + +\makeatletter + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% LyX specific LaTeX commands. +%% Because html converters don't know tabularnewline +\providecommand{\tabularnewline}{\\} + +\makeatother + +\usepackage{babel} +\begin{document} +\begin{longtable}{|c|c|c|c|c|c|c|c|c|c|c|c|} +\hline +fh1 & fh2 & fh3 & fh4 & fh8 & fh9 & fh10 & fh11 & fh12 & fh13 & fh14 & fh18\tabularnewline +\endfirsthead +\hline +h1 & h2 & h3 & h4 & h8 & h9 & h10 & h11 & h12 & h13 & h14 & h18\tabularnewline +\endhead +\hline +f1 & f2 & f3 & f4 & f8 & f9 & f10 & f11 & f12 & f13 & f14 & f18\tabularnewline +\hline +a1 & a2 & a3 & a4 & a8 & a9 & a10 & a11 & a12 & a13 & a14 & a18\tabularnewline +\hline +\hline +b1 & b2 & \multicolumn{2}{c|}{b3} & b8 & b9 & b10 & b11 & b12 & b13 & b14 & \multicolumn{1}{c|}{b15}\tabularnewline +\hline +\multicolumn{4}{|c|}{c1} & c8 & c9 & \multicolumn{3}{c|}{c10} & c13 & c14 & c18\tabularnewline +\hline +d1 & d2 & d3 & \multicolumn{6}{c|}{d4} & d13 & d14 & \multicolumn{1}{c|}{d16}\tabularnewline +\hline +\multicolumn{4}{|c|}{e1} & e8 & e9 & e10 & e11 & \multicolumn{3}{c|}{e12} & e18\tabularnewline +\hline +g1 & g2 & \multicolumn{9}{c|}{g3} & g18\tabularnewline +\hline +\multicolumn{11}{|c|}{i1} & i3\tabularnewline +\hline +j1 & \multicolumn{11}{c|}{j2}\tabularnewline +\hline +\end{longtable}\begin{longtable}{|c|c|c|c|c|c|c|c|c|} +\hline +fh1 & fh2 & fh5 & fh6 & fh8 & fh9 & fh13 & fh15 & fh18\tabularnewline +\endfirsthead +\hline +h1 & h2 & h5 & h6 & h8 & h9 & h13 & h15 & h18\tabularnewline +\endhead +\hline +f1 & f2 & f5 & f6 & f8 & f9 & f13 & f15 & f18\tabularnewline +\hline +a1 & a2 & a5 & a6 & a8 & a9 & a13 & a15 & a18\tabularnewline +\hline +\hline +b1 & b2 & \multicolumn{2}{c|}{b3} & b8 & b9 & b13 & \multicolumn{2}{c|}{b15}\tabularnewline +\hline +\multicolumn{2}{|c|}{c1} & c5 & c6 & c8 & c9 & c13 & c15 & c18\tabularnewline +\hline +d1 & d2 & \multicolumn{4}{c|}{d4} & d13 & d15 & \multicolumn{1}{c|}{d16}\tabularnewline +\hline +\multicolumn{3}{|c|}{e1} & e6 & e8 & e9 & \multicolumn{2}{c|}{e12} & e18\tabularnewline +\hline +g1 & g2 & \multicolumn{6}{c|}{g3} & g18\tabularnewline +\hline +\multicolumn{8}{|c|}{i1} & i3\tabularnewline +\hline +j1 & \multicolumn{8}{c|}{j2}\tabularnewline +\hline +\end{longtable}\begin{longtable}{|c|c|c|c|c|c|c|c|} +\hline +fh1 & fh2 & fh7 & fh8 & fh9 & fh13 & fh16 & fh18\tabularnewline +\endfirsthead +\hline +h1 & h2 & h7 & h8 & h9 & h13 & h16 & h18\tabularnewline +\endhead +\hline +f1 & f2 & f7 & f8 & f9 & f13 & f16 & f18\tabularnewline +\hline +a1 & a2 & a7 & a8 & a9 & a13 & a16 & a18\tabularnewline +\hline +\hline +b1 & b2 & \multicolumn{1}{c|}{b3} & b8 & b9 & b13 & \multicolumn{2}{c|}{b15}\tabularnewline +\hline +\multicolumn{2}{|c|}{c1} & c7 & c8 & c9 & c13 & c16 & c18\tabularnewline +\hline +d1 & d2 & \multicolumn{3}{c|}{d4} & d13 & \multicolumn{2}{c|}{d16}\tabularnewline +\hline +\multicolumn{2}{|c|}{e1} & e7 & e8 & e9 & \multicolumn{1}{c|}{e12} & e16 & e18\tabularnewline +\hline +g1 & g2 & \multicolumn{5}{c|}{g3} & g18\tabularnewline +\hline +\multicolumn{7}{|c|}{i1} & i3\tabularnewline +\hline +j1 & \multicolumn{7}{c|}{j2}\tabularnewline +\hline +\end{longtable}\begin{longtable}{|c|c|c|c|c|c|} +\hline +fh1 & fh2 & fh9 & fh13 & fh17 & fh18\tabularnewline +\endfirsthead +\hline +h1 & h2 & h9 & h13 & h17 & h18\tabularnewline +\endhead +\hline +f1 & f2 & f9 & f13 & f17 & f18\tabularnewline +\hline +a1 & a2 & a9 & a13 & a17 & a18\tabularnewline +\hline +\hline +b1 & b2 & b9 & b13 & \multicolumn{2}{c|}{b15}\tabularnewline +\hline +\multicolumn{2}{|c|}{c1} & c9 & c13 & c17 & c18\tabularnewline +\hline +d1 & d2 & \multicolumn{1}{c|}{d4} & d13 & \multicolumn{2}{c|}{d16}\tabularnewline +\hline +\multicolumn{2}{|c|}{e1} & e9 & \multicolumn{1}{c|}{e12} & e17 & e18\tabularnewline +\hline +g1 & g2 & \multicolumn{2}{c|}{g3} & g17 & g18\tabularnewline +\hline +\multicolumn{4}{|c|}{i1} & i2 & i3\tabularnewline +\hline +j1 & \multicolumn{5}{c|}{j2}\tabularnewline +\hline +\end{longtable} +\end{document} diff --git a/test/tex/limits_table1.tex b/test/tex/limits_table1.tex new file mode 100644 index 0000000..2fa3c70 --- /dev/null +++ b/test/tex/limits_table1.tex @@ -0,0 +1,46 @@ +%% LyX 2.0.3 created this file. For more info, see http://www.lyx.org/. +%% Do not edit unless you really know what you are doing. +\documentclass[english]{article} +\usepackage[T1]{fontenc} +\usepackage[latin9]{inputenc} +\usepackage{longtable} + +\makeatletter + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% LyX specific LaTeX commands. +%% Because html converters don't know tabularnewline +\providecommand{\tabularnewline}{\\} + +\makeatother + +\usepackage{babel} +\begin{document} +\begin{longtable}{|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|c|} +\hline +fh1 & fh2 & fh3 & fh4 & fh5 & fh6 & fh7 & fh8 & fh9 & fh10 & fh11 & fh12 & fh13 & fh14 & fh15 & fh16 & fh17 & fh18\tabularnewline +\endfirsthead +\hline +h1 & h2 & h3 & h4 & h5 & h6 & h7 & h8 & h9 & h10 & h11 & h12 & h13 & h14 & h15 & h16 & h17 & h18\tabularnewline +\endhead +\hline +f1 & f2 & f3 & f4 & f5 & f6 & f7 & f8 & f9 & f10 & f11 & f12 & f13 & f14 & f15 & f16 & f17 & f18\tabularnewline +\hline +a1 & a2 & a3 & a4 & a5 & a6 & a7 & a8 & a9 & a10 & a11 & a12 & a13 & a14 & a15 & a16 & a17 & a18\tabularnewline +\hline +\hline +b1 & b2 & \multicolumn{5}{c|}{b3} & b8 & b9 & b10 & b11 & b12 & b13 & b14 & \multicolumn{4}{c|}{b15}\tabularnewline +\hline +\multicolumn{4}{|c|}{c1} & c5 & c6 & c7 & c8 & c9 & \multicolumn{3}{c|}{c10} & c13 & c14 & c15 & c16 & c17 & c18\tabularnewline +\hline +d1 & d2 & d3 & \multicolumn{9}{c|}{d4} & d13 & d14 & d15 & \multicolumn{3}{c|}{d16}\tabularnewline +\hline +\multicolumn{5}{|c|}{e1} & e6 & e7 & e8 & e9 & e10 & e11 & \multicolumn{4}{c|}{e12} & e16 & e17 & e18\tabularnewline +\hline +g1 & g2 & \multicolumn{14}{c|}{g3} & g17 & g18\tabularnewline +\hline +\multicolumn{16}{|c|}{i1} & i2 & i3\tabularnewline +\hline +j1 & \multicolumn{17}{c|}{j2}\tabularnewline +\hline +\end{longtable} +\end{document}