├── config ├── dev.exs ├── prod.exs ├── test.exs └── config.exs ├── test ├── test_helper.exs ├── cobol_to_elixir │ ├── csis_cobol_examples │ │ ├── about.txt │ │ └── iterif_test.exs │ ├── procedure_division_test.exs │ ├── util_test.exs │ ├── conversion_warnings_test.exs │ ├── procedure_division │ │ ├── paragraph_perform_test.exs │ │ └── display_test.exs │ ├── file_test.exs │ ├── identification_division_test.exs │ ├── data_division │ │ └── group_items_test.exs │ └── data_division_test.exs ├── cobol_examples │ ├── coboltut4.cob │ └── coboltut1.cob ├── cobol_to_elixir_test.exs └── support │ └── cobol_to_elixir_case.ex ├── coveralls.json ├── .formatter.exs ├── lib ├── parsed │ └── variable.ex ├── cobol_to_elixir │ ├── parsed.ex │ ├── util.ex │ ├── tokenizer.ex │ ├── elixirizer.ex │ └── parser.ex └── cobol_to_elixir.ex ├── .gitignore ├── LICENSE ├── README.md ├── mix.exs ├── mix.lock └── livebook_examples.livemd /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :warn 4 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :warn 4 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/csis_cobol_examples/about.txt: -------------------------------------------------------------------------------- 1 | These examples can be found at http://www.csis.ul.ie/cobol/examples/ -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage_options": { 3 | "treat_no_relevant_lines_as_covered": true 4 | }, 5 | "skip_files": [] 6 | } -------------------------------------------------------------------------------- /test/cobol_to_elixir/procedure_division_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.ProcedureDivisionTest do 2 | use CobolToElixirCase 3 | end 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | line_length: 120 5 | ] 6 | -------------------------------------------------------------------------------- /lib/parsed/variable.ex: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.Parsed.Variable do 2 | defstruct depth: nil, name: nil, type: :single, pic: nil, value: nil, children: [], constant: nil 3 | end 4 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :info 4 | 5 | if Mix.env() == :dev do 6 | config :mix_test_watch, 7 | exclude: [~r/.*temp_delete_contents.*/] 8 | end 9 | 10 | import_config "#{config_env()}.exs" 11 | -------------------------------------------------------------------------------- /lib/cobol_to_elixir/parsed.ex: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.Parsed do 2 | defstruct program_id: nil, 3 | author: nil, 4 | date_written: nil, 5 | divisions: [], 6 | file_control: [], 7 | file_variables: %{}, 8 | procedure: [], 9 | paragraphs: %{}, 10 | variables: [], 11 | variable_map: %{} 12 | end 13 | -------------------------------------------------------------------------------- /test/cobol_examples/coboltut4.cob: -------------------------------------------------------------------------------- 1 | >>SOURCE FORMAT FREE 2 | IDENTIFICATION DIVISION. 3 | PROGRAM-ID. coboltut. 4 | AUTHOR. Mike Binns. 5 | DATE-WRITTEN.March 19th 2021 6 | ENVIRONMENT DIVISION. 7 | CONFIGURATION SECTION. 8 | SPECIAL-NAMES. 9 | DATA DIVISION. 10 | FILE SECTION. 11 | WORKING-STORAGE SECTION. 12 | 13 | 14 | PROCEDURE DIVISION. 15 | SubOne. 16 | DISPLAY "In Paragraph 1" 17 | PERFORM SubTwo 18 | DISPLAY "Returned to Paragraph 1" 19 | PERFORM SubFour 2 TIMES. 20 | STOP RUN. 21 | 22 | SubThree. 23 | DISPLAY "In Paragraph 3". 24 | 25 | SubTwo. 26 | DISPLAY "In Paragraph 2" 27 | PERFORM SubThree 28 | DISPLAY "Returned to Paragraph 2". 29 | 30 | SubFour. 31 | DISPLAY "Repeat". 32 | 33 | STOP RUN. 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | cobol_to_elixir-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | /test/temp_delete_contents/ -------------------------------------------------------------------------------- /lib/cobol_to_elixir.ex: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir do 2 | alias CobolToElixir.Tokenizer 3 | alias CobolToElixir.Parser 4 | alias CobolToElixir.Elixirizer 5 | 6 | def convert_file!(cobol_file, output_file) do 7 | contents = File.read!(cobol_file) 8 | {:ok, elixir} = convert(contents) 9 | File.write!(output_file, elixir) 10 | end 11 | 12 | def convert!(contents, opts \\ []) do 13 | {:ok, elixir} = convert(contents, opts) 14 | elixir 15 | end 16 | 17 | def convert(contents, opts \\ []) do 18 | {:ok, tokenized} = Tokenizer.tokenize(contents) 19 | # IO.inspect(tokenized) 20 | {:ok, parsed} = Parser.parse(tokenized) 21 | # IO.inspect(parsed) 22 | {:ok, elixir} = Elixirizer.elixirize(parsed, opts) 23 | # IO.puts(elixir) 24 | {:ok, elixir} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/util_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.UtilTest do 2 | use CobolToElixirCase 3 | 4 | test "execute_cobol_code!/2 succeeds" do 5 | cobol = """ 6 | >>SOURCE FORMAT FREE 7 | IDENTIFICATION DIVISION. 8 | PROGRAM-ID. test1. 9 | AUTHOR. Mike Binns. 10 | DATE-WRITTEN.March 19th 2021 11 | PROCEDURE DIVISION. 12 | DISPLAY "Hello" " " "World" 13 | STOP RUN. 14 | 15 | """ 16 | 17 | validate_cobol_code(cobol) 18 | 19 | assert CobolToElixir.Util.execute_cobol_code!(cobol) == %{output: "Hello World\n", files: %{}} 20 | end 21 | 22 | test "execute_cobol_code!/2 raises on error" do 23 | cobol = """ 24 | >>SOURCE FORMAT FREE 25 | THIS IS NOT A VALID COBOL FILE 26 | """ 27 | 28 | assert_raise RuntimeError, ~r/^Error compiling cobol/, fn -> CobolToElixir.Util.execute_cobol_code!(cobol) end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/conversion_warnings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.ConversionWarningsTest do 2 | use CobolToElixirCase 3 | 4 | import ExUnit.CaptureLog 5 | 6 | test "warns on untokenizable lines" do 7 | cobol = """ 8 | IDENTIFICATION DIVISION. 9 | PROGRAM-ID. test1. 10 | WHATISTHISLINE ITS NOT COBOL. 11 | PROCEDURE DIVISION. 12 | """ 13 | 14 | log = capture_log(fn -> CobolToElixir.convert!(cobol) end) 15 | assert log =~ "[warn] Unable to tokenize line: WHATISTHISLINE ITS NOT COBOL\n" 16 | end 17 | 18 | test "warns on unknown division" do 19 | cobol = """ 20 | IDENTIFICATION DIVISION. 21 | PROGRAM-ID. test1. 22 | MULTIPLICATION DIVISION. 23 | PROCEDURE DIVISION. 24 | """ 25 | 26 | log = capture_log(fn -> CobolToElixir.convert!(cobol) end) 27 | assert log =~ "[warn] No parser for division MULTIPLICATION\n" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/cobol_examples/coboltut1.cob: -------------------------------------------------------------------------------- 1 | >>SOURCE FORMAT FREE 2 | IDENTIFICATION DIVISION. 3 | PROGRAM-ID. coboltut. 4 | AUTHOR. Mike Binns. 5 | DATE-WRITTEN.March 19th 2021 6 | ENVIRONMENT DIVISION. 7 | 8 | DATA DIVISION. 9 | FILE SECTION. 10 | WORKING-STORAGE SECTION. 11 | 01 UserName PIC X(30) VALUE "You". 12 | 01 Num1 PIC 9 VALUE ZEROS. 13 | 01 Num2 PIC 9 VALUE ZEROS. 14 | 01 Spaces1 PIC XXX VALUE SPACES. 15 | 01 Total PIC 99 VALUE 0. 16 | 01 SSNum. 17 | 02 SSArea PIC 999. 18 | 02 SSGroup PIC 99. 19 | 02 SSSerial PIC 9999. 20 | 01 PIValue CONSTANT AS 3.14. 21 | *> ZERO, zeros 22 | *> SPACE SPACES 23 | *> HIGH-VALUE HIGH-VALUES 24 | *> LOW-VALUE LOW-VALUES 25 | 26 | 27 | 28 | PROCEDURE DIVISION. 29 | 30 | DISPLAY "What is your name" WITH NO ADVANCING 31 | ACCEPT UserName 32 | DISPLAY "Hello " UserName 33 | 34 | MOVE ZERO TO UserName 35 | DISPLAY UserName 36 | DISPLAY "Enter 2 values to sum " 37 | ACCEPT Num1 38 | ACCEPT Num2 39 | COMPUTE Total = Num1 + Num2 40 | DISPLAY Num1 " + " Num2 " = " Total 41 | DISPLAY "Enter your ssn " 42 | ACCEPT SSNum 43 | DISPLAY "Area " SSArea 44 | 45 | STOP RUN. 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mike Binns 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/procedure_division/paragraph_perform_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.ProcedureDivision.ParagraphPerformTest do 2 | use CobolToElixirCase 3 | 4 | test "handles paragraphs/performs" do 5 | cobol = """ 6 | >>SOURCE FORMAT FREE 7 | IDENTIFICATION DIVISION. 8 | PROGRAM-ID. proceduretest. 9 | AUTHOR. Mike Binns. 10 | DATE-WRITTEN.March 19th 2021. 11 | PROCEDURE DIVISION. 12 | SubOne. 13 | DISPLAY "In Paragraph 1" 14 | PERFORM SubTwo 15 | DISPLAY "Returned to Paragraph 1" 16 | PERFORM SubFour 2 TIMES. 17 | STOP RUN. 18 | 19 | SubThree. 20 | DISPLAY "In Paragraph 3". 21 | 22 | SubTwo. 23 | DISPLAY "In Paragraph 2" 24 | PERFORM SubThree 25 | DISPLAY "Returned to Paragraph 2". 26 | 27 | SubFour. 28 | DISPLAY "Repeat". 29 | 30 | STOP RUN. 31 | 32 | """ 33 | 34 | validate_cobol_code(cobol) 35 | 36 | expected = 37 | "In Paragraph 1\nIn Paragraph 2\nIn Paragraph 3\nReturned to Paragraph 2\nReturned to Paragraph 1\nRepeat\n" 38 | 39 | assert_output_equal(cobol, ElixirFromCobol.Proceduretest, output: expected) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/file_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.FileTest do 2 | use CobolToElixirCase 3 | 4 | test "file writing" do 5 | cobol = """ 6 | >>SOURCE FORMAT FREE 7 | IDENTIFICATION DIVISION. 8 | PROGRAM-ID. foo123. 9 | ENVIRONMENT DIVISION. 10 | INPUT-OUTPUT SECTION. 11 | FILE-CONTROL. 12 | SELECT CustomerFile ASSIGN TO "Customer.dat" 13 | ORGANIZATION IS LINE SEQUENTIAL 14 | ACCESS IS SEQUENTIAL. 15 | DATA DIVISION. 16 | FILE SECTION. 17 | FD CustomerFile. 18 | 01 CustomerData. 19 | 02 IDNum PIC 9(5). 20 | 02 CustName. 21 | 03 FirstName PIC X(15). 22 | 03 LastName PIC X(15). 23 | WORKING-STORAGE SECTION. 24 | 01 WSCustomerData. 25 | 02 WSIDNum PIC 9(5). 26 | 02 WSCustName. 27 | 03 WSFirstName PIC X(15). 28 | 03 WSLastName PIC X(15). 29 | 30 | PROCEDURE DIVISION. 31 | OPEN OUTPUT CustomerFile. 32 | MOVE 00001 TO IDNum. 33 | MOVE 'Doug' TO FirstName. 34 | MOVE "Thomas" TO LastName. 35 | WRITE CustomerData 36 | END-WRITE. 37 | CLOSE CustomerFile. 38 | STOP RUN. 39 | """ 40 | 41 | validate_cobol_code(cobol) 42 | 43 | assert %{output: "", files: %{"Customer.dat" => "00001Doug Thomas\n"}} = execute_cobol_code!(cobol) 44 | 45 | assert_output_equal(cobol, ElixirFromCobol.Foo123) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/procedure_division/display_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.ProcedureDivision.DisplayTest do 2 | use CobolToElixirCase 3 | 4 | test "displays simple string" do 5 | cobol = """ 6 | >>SOURCE FORMAT FREE 7 | IDENTIFICATION DIVISION. 8 | PROGRAM-ID. foo123. 9 | PROCEDURE DIVISION. 10 | DISPLAY "Hello". 11 | """ 12 | 13 | validate_cobol_code(cobol) 14 | 15 | assert CobolToElixir.convert!(cobol) =~ ~s|IO.puts "Hello"| 16 | end 17 | 18 | test "displays variable" do 19 | cobol = """ 20 | >>SOURCE FORMAT FREE 21 | IDENTIFICATION DIVISION. 22 | PROGRAM-ID. foo123. 23 | DATA DIVISION. 24 | WORKING-STORAGE SECTION. 25 | 01 NAME PIC X(4) VALUE "JOE". 26 | PROCEDURE DIVISION. 27 | DISPLAY NAME. 28 | """ 29 | 30 | validate_cobol_code(cobol) 31 | 32 | assert CobolToElixir.convert!(cobol) =~ ~s|IO.puts var_NAME| 33 | end 34 | 35 | test "displays with no advancing" do 36 | cobol = """ 37 | >>SOURCE FORMAT FREE 38 | IDENTIFICATION DIVISION. 39 | PROGRAM-ID. foo123. 40 | DATA DIVISION. 41 | WORKING-STORAGE SECTION. 42 | 01 NAME PIC X(4) VALUE "JOE". 43 | PROCEDURE DIVISION. 44 | DISPLAY NAME WITH NO ADVANCING. 45 | """ 46 | 47 | validate_cobol_code(cobol) 48 | 49 | assert CobolToElixir.convert!(cobol) =~ ~s|IO.write var_NAME| 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CobolToElixir 2 | [![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2FTheFirstAvenger%2Fcobol_to_elixir%2Fblob%2Fmaster%2Flivebook_examples.livemd) 3 | 4 | Roadmap: 5 | 6 | - Features 7 | - Identification Division 8 | - [x] Program ID -> Module Name 9 | - [x] Author -> Note in Moduledoc 10 | - [x] Date-Written -> Note in Moduledoc 11 | - Working-Storage Section 12 | - [x] Parse String Variable 13 | - [x] Parse Simple Number Variable 14 | - [ ] Parse Complex Number Variable 15 | - [x] Parse Group Items (nested maps) 16 | - File Access 17 | - [ ] Parse file-control 18 | - [ ] Parse file section 19 | - [ ] Write (Open Output) 20 | - [ ] Read (Open Input) 21 | - Procedure Division 22 | - [x] Initialize Variables 23 | - [x] Display 24 | - [x] String 25 | - [x] Simple Variable 26 | - [x] Group Item (nested map) 27 | - [x] Accept 28 | - [ ] Move 29 | - [x] Into Simple Variable 30 | - [x] Into Group Items 31 | - [ ] Compute 32 | - [ ] If/Else 33 | - [x] Internal Subroutines (Perform) 34 | - [ ] External Subroutines (Call) 35 | - Testing Framework 36 | - [x] Compile and execute COBOL code 37 | - [x] Compile and execute Elixir code 38 | - [x] Support specifying input (stdio) 39 | - [x] Support comparing output (stdio) 40 | - [ ] Support specifying external files for input 41 | - [ ] Support comparing external output files on completion 42 | 43 | ## Installation 44 | 45 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 46 | by adding `cobol_to_elixir` to your list of dependencies in `mix.exs`: 47 | 48 | ```elixir 49 | def deps do 50 | [ 51 | {:cobol_to_elixir, "~> 0.1.0"} 52 | ] 53 | end 54 | ``` 55 | 56 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 57 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 58 | be found at [https://hexdocs.pm/cobol_to_elixir](https://hexdocs.pm/cobol_to_elixir). 59 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/csis_cobol_examples/iterif_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.CSISCOBOLExamples.IterifTest do 2 | use CobolToElixirCase 3 | 4 | # Need to add support for inline perform, currently only support `perform [procedure_name]` 5 | @tag :skip 6 | test "iterif.cbl" do 7 | cobol = """ 8 | >> SOURCE FORMAT FREE 9 | *> replaced below line with above line to get it to compile 10 | *> $ SET SOURCEFORMAT"FREE" 11 | IDENTIFICATION DIVISION. 12 | PROGRAM-ID. Iteration-If. 13 | AUTHOR. Michael Coughlan. 14 | 15 | DATA DIVISION. 16 | WORKING-STORAGE SECTION. 17 | 01 Num1 PIC 9 VALUE ZEROS. 18 | 01 Num2 PIC 9 VALUE ZEROS. 19 | 01 Result PIC 99 VALUE ZEROS. 20 | 01 Operator PIC X VALUE SPACE. 21 | 22 | PROCEDURE DIVISION. 23 | Calculator. 24 | PERFORM 3 TIMES 25 | DISPLAY "Enter First Number : " WITH NO ADVANCING 26 | ACCEPT Num1 27 | DISPLAY "Enter Second Number : " WITH NO ADVANCING 28 | ACCEPT Num2 29 | DISPLAY "Enter operator (+ or *) : " WITH NO ADVANCING 30 | ACCEPT Operator 31 | IF Operator = "+" THEN 32 | ADD Num1, Num2 GIVING Result 33 | END-IF 34 | IF Operator = "*" THEN 35 | MULTIPLY Num1 BY Num2 GIVING Result 36 | END-IF 37 | DISPLAY "Result is = ", Result 38 | END-PERFORM. 39 | STOP RUN. 40 | """ 41 | 42 | validate_cobol_code(cobol) 43 | 44 | input = [{100, 5}, {100, 6}, {100, "+"}, {100, 5}, {100, 6}, {100, "+"}, {100, 5}, {100, 6}, {100, "+"}] 45 | 46 | expected_output = 47 | "Enter First Number : Enter Second Number : Enter operator (+ or *) : Result is = 11\nEnter First Number : Enter Second Number : Enter operator (+ or *) : Result is = 11\nEnter First Number : Enter Second Number : Enter operator (+ or *) : Result is = 11\n" 48 | 49 | assert_output_equal(cobol, ElixirFromCobol.Test1, output: expected_output, input: input) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | test_coverage: [tool: ExCoveralls], 7 | preferred_cli_env: [ 8 | coveralls: :test, 9 | "coveralls.detail": :test, 10 | "coveralls.post": :test, 11 | "coveralls.html": :test, 12 | coverage_report: :test 13 | ], 14 | app: :cobol_to_elixir, 15 | version: "0.1.0", 16 | elixir: "~> 1.12", 17 | elixirc_paths: elixirc_paths(Mix.env()), 18 | start_permanent: Mix.env() == :prod, 19 | deps: deps(), 20 | aliases: aliases(), 21 | source_url: "https://github.com/TheFirstAvenger/cobol_to_elixir", 22 | package: package() 23 | ] 24 | end 25 | 26 | # Run "mix help compile.app" to learn about applications. 27 | def application do 28 | [ 29 | extra_applications: [:logger] 30 | ] 31 | end 32 | 33 | # Specifies which paths to compile per environment. 34 | defp elixirc_paths(:test), do: ["lib", "test/support"] 35 | defp elixirc_paths(_), do: ["lib"] 36 | 37 | # Run "mix help deps" to learn about dependencies. 38 | defp deps do 39 | [ 40 | {:excoveralls, "~> 0.14.0", only: :test}, 41 | {:mix_test_watch, "~> 1.0", only: :dev, runtime: false}, 42 | {:ex_doc, "~> 0.25.1"} 43 | # {:dep_from_hexpm, "~> 0.3.0"}, 44 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 45 | ] 46 | end 47 | 48 | defp aliases do 49 | [ 50 | coverage_report: [&coverage_report/1] 51 | ] 52 | end 53 | 54 | defp package do 55 | [ 56 | licenses: ["MIT"], 57 | links: %{"GitHub" => "https://github.com/TheFirstAvenger/cobol_to_elixir"} 58 | ] 59 | end 60 | 61 | defp coverage_report(_) do 62 | Mix.Task.run("coveralls.html") 63 | 64 | open_cmd = 65 | case :os.type() do 66 | {:win32, _} -> 67 | "start" 68 | 69 | {:unix, :darwin} -> 70 | "open" 71 | 72 | {:unix, _} -> 73 | "xdg-open" 74 | end 75 | 76 | System.cmd(open_cmd, ["cover/excoveralls.html"]) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/identification_division_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.IdentificationDivisionTest do 2 | use CobolToElixirCase 3 | 4 | describe "Program-ID" do 5 | test "Sets module name correctly" do 6 | cobol = """ 7 | >>SOURCE FORMAT FREE 8 | IDENTIFICATION DIVISION. 9 | PROGRAM-ID. foo123. 10 | """ 11 | 12 | validate_cobol_code(cobol) 13 | 14 | assert CobolToElixir.convert!(cobol) =~ "defmodule ElixirFromCobol.Foo123 do" 15 | end 16 | 17 | test "Overrides Namespace" do 18 | cobol = """ 19 | >>SOURCE FORMAT FREE 20 | IDENTIFICATION DIVISION. 21 | PROGRAM-ID. foo123. 22 | """ 23 | 24 | validate_cobol_code(cobol) 25 | 26 | assert CobolToElixir.convert!(cobol, namespace: "MyModule") =~ "defmodule MyModule.Foo123 do" 27 | end 28 | end 29 | 30 | describe "Author" do 31 | test "Renders in moduledoc" do 32 | cobol = """ 33 | >>SOURCE FORMAT FREE 34 | IDENTIFICATION DIVISION. 35 | PROGRAM-ID. foo123. 36 | AUTHOR. John Doe. 37 | """ 38 | 39 | validate_cobol_code(cobol) 40 | 41 | assert CobolToElixir.convert!(cobol) =~ "@moduledoc \"\"\"\n author: John Doe\n" 42 | end 43 | 44 | test "n/a when missing" do 45 | cobol = """ 46 | >>SOURCE FORMAT FREE 47 | IDENTIFICATION DIVISION. 48 | PROGRAM-ID. foo123. 49 | """ 50 | 51 | validate_cobol_code(cobol) 52 | 53 | assert CobolToElixir.convert!(cobol) =~ "@moduledoc \"\"\"\n author: n/a\n" 54 | end 55 | end 56 | 57 | describe "Date Written" do 58 | test "Renders in moduledoc" do 59 | cobol = """ 60 | >>SOURCE FORMAT FREE 61 | IDENTIFICATION DIVISION. 62 | PROGRAM-ID. foo123. 63 | DATE-WRITTEN. January 1st, 2021. 64 | """ 65 | 66 | validate_cobol_code(cobol) 67 | 68 | assert CobolToElixir.convert!(cobol) =~ "@moduledoc \"\"\"\n author: n/a\n date written: January 1st, 2021\n" 69 | end 70 | 71 | test "n/a when missing" do 72 | cobol = """ 73 | >>SOURCE FORMAT FREE 74 | IDENTIFICATION DIVISION. 75 | PROGRAM-ID. foo123. 76 | """ 77 | 78 | validate_cobol_code(cobol) 79 | 80 | assert CobolToElixir.convert!(cobol) =~ "@moduledoc \"\"\"\n author: n/a\n date written: n/a\n" 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/cobol_to_elixir/util.ex: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.Util do 2 | def execute_cobol_code!(cobol, input \\ []) do 3 | tmp_folder = generate_tmp_folder() 4 | 5 | try do 6 | case compile_cobol(cobol, tmp_folder) do 7 | {"", 0} -> :ok 8 | {output, 1} -> raise "Error compiling cobol:\n#{output}" 9 | end 10 | 11 | starting_files = File.ls!(tmp_folder) 12 | 13 | port = Port.open({:spawn, "cobol"}, [{:cd, tmp_folder}, :binary, :exit_status, :stderr_to_stdout]) 14 | 15 | send_cobol_input(port, input) 16 | 17 | # :timer.sleep(1000) 18 | # Port.close(port) 19 | output = get_cobol_output(port) |> Enum.reverse() |> Enum.join("") 20 | # {output, 0} = System.cmd("./#{tmp_folder}/cobol", [], stderr_to_stdout: true) 21 | ending_files = File.ls!(tmp_folder) 22 | 23 | new_files = get_new_files(starting_files, ending_files, tmp_folder) 24 | 25 | %{output: output, files: new_files} 26 | after 27 | File.rm_rf!(tmp_folder) 28 | end 29 | end 30 | 31 | def compile_cobol(code, tmp_folder) do 32 | cobol_path = Path.join(tmp_folder, "cobol.cob") 33 | cobol_executable_path = Path.join(tmp_folder, "cobol") 34 | File.write(cobol_path, code) 35 | System.cmd("cobc", ["-x", "-o", cobol_executable_path, cobol_path], stderr_to_stdout: true) 36 | end 37 | 38 | def generate_tmp_folder do 39 | tmp_folder = Path.relative_to_cwd("test/temp_delete_contents/#{Enum.random(1000..1_000_000_000_000)}") 40 | File.mkdir_p!(tmp_folder) 41 | tmp_folder 42 | end 43 | 44 | defp get_cobol_output(port, acc \\ []) do 45 | # coveralls-ignore-start 46 | receive do 47 | {^port, {:data, output}} -> get_cobol_output(port, [output | acc]) 48 | {^port, {:closed}} -> acc 49 | {^port, {:exit_status, 0}} -> acc 50 | other -> raise "got unexpected port response: #{inspect(other)}" 51 | end 52 | 53 | # coveralls-ignore-end 54 | end 55 | 56 | defp send_cobol_input(_port, []), do: :ok 57 | 58 | defp send_cobol_input(port, [{timeout, input} | tail]) do 59 | :timer.sleep(timeout) 60 | Port.command(port, "#{input}\n") 61 | send_cobol_input(port, tail) 62 | end 63 | 64 | def get_new_files(starting_files, ending_files, tmp_folder) do 65 | starting_files 66 | |> Enum.reduce(ending_files, &List.delete(&2, &1)) 67 | |> Enum.map(fn file -> 68 | {file, File.read!(Path.join(tmp_folder, file))} 69 | end) 70 | |> Enum.into(%{}) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/cobol_to_elixir_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixirTest do 2 | use CobolToElixirCase 3 | 4 | @moduletag timeout: 10000 5 | 6 | test "convert_file!/2" do 7 | tmp_folder = Path.relative_to_cwd("test/temp_delete_contents/#{Enum.random(1000..1_000_000_000_000)}") 8 | 9 | try do 10 | File.mkdir_p!(tmp_folder) 11 | 12 | cobol_text = """ 13 | >>SOURCE FORMAT FREE 14 | IDENTIFICATION DIVISION. 15 | PROGRAM-ID. test1. 16 | AUTHOR. Mike Binns. 17 | DATE-WRITTEN.March 19th 2021 18 | DATA DIVISION. 19 | WORKING-STORAGE SECTION. 20 | 01 NAME PIC X(5). 21 | PROCEDURE DIVISION. 22 | STOP RUN. 23 | """ 24 | 25 | cobol_file = Path.join(tmp_folder, "cobol.cob") 26 | elixir_file = Path.join(tmp_folder, "module.ex") 27 | File.write!(cobol_file, cobol_text) 28 | CobolToElixir.convert_file!(cobol_file, elixir_file) 29 | elixir_text = File.read!(elixir_file) 30 | assert elixir_text =~ "defmodule ElixirFromCobol.Test1" 31 | after 32 | File.rm_rf!(tmp_folder) 33 | end 34 | end 35 | 36 | test "validate testing framework" do 37 | cobol = """ 38 | >>SOURCE FORMAT FREE 39 | IDENTIFICATION DIVISION. 40 | PROGRAM-ID. test1. 41 | AUTHOR. Mike Binns. 42 | DATE-WRITTEN.March 19th 2021 43 | DATA DIVISION. 44 | WORKING-STORAGE SECTION. 45 | 01 NAME PIC X(5). 46 | PROCEDURE DIVISION. 47 | MOVE "Mike" TO NAME 48 | DISPLAY "Hello " NAME 49 | ACCEPT NAME 50 | DISPLAY "Hello " NAME 51 | DISPLAY "Hello " NAME WITH NO ADVANCING 52 | STOP RUN. 53 | 54 | """ 55 | 56 | validate_cobol_code(cobol) 57 | 58 | input = [{1000, "John"}] 59 | expected_output = "Hello Mike \nHello John \nHello John " 60 | # First verify our cobol code runs and returns the right value 61 | assert %{output: expected_output, files: %{}} == execute_cobol_code!(cobol, input) 62 | # Next run our converter to generate Elixir code 63 | assert {:ok, elixir_code} = CobolToElixir.convert(cobol, accept_via_message: true) 64 | # Now run that Elixir code, and ensure the output matches our expected output 65 | assert %{output: expected_output, files: nil} == execute_elixir_code(elixir_code, ElixirFromCobol.Test1, input) 66 | 67 | # The below code is a one liner version of all of the above commands. Lets make sure that works too. 68 | assert_output_equal(cobol, ElixirFromCobol.Test1, output: expected_output, input: input) 69 | 70 | # make sure validate_cobol_code raises on bad cobol 71 | assert_raise RuntimeError, ~r/^Error compiling cobol:/, fn -> validate_cobol_code("some bad code") end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/support/cobol_to_elixir_case.ex: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixirCase do 2 | import ExUnit.CaptureIO 3 | 4 | alias CobolToElixir.Util 5 | 6 | require ExUnit.Assertions 7 | require Logger 8 | 9 | defmacro __using__([]) do 10 | quote do 11 | use ExUnit.Case 12 | import CobolToElixirCase 13 | end 14 | end 15 | 16 | @doc """ 17 | Compiles the cobol code and ensures there are no errors/warnings 18 | """ 19 | def validate_cobol_code(cobol) do 20 | tmp_folder = Util.generate_tmp_folder() 21 | 22 | try do 23 | case Util.compile_cobol(cobol, tmp_folder) do 24 | {"", 0} -> :ok 25 | {output, 1} -> raise "Error compiling cobol:\n#{output}" 26 | end 27 | after 28 | File.rm_rf!(tmp_folder) 29 | end 30 | end 31 | 32 | @doc """ 33 | Compiles and then runs the given cobol code with the optional list of inputs being sent. 34 | Input is a list of timeout/command values, e.g. [{1000, "John"}, {500, "Mike"}] 35 | would send "John" after 1 second, then "Mike" after another half a second. 36 | 37 | Returns map containing program output and any new files. 38 | """ 39 | def execute_cobol_code!(cobol, input \\ []) do 40 | Util.execute_cobol_code!(cobol, input) 41 | end 42 | 43 | @doc """ 44 | Compiles and loads the given code into memory, runs the module, and unloads the module. 45 | Accepts list of input same as `execute_cobol_code!/2`. 46 | 47 | Returns map containing program output and any new files. 48 | """ 49 | def execute_elixir_code(str, module, input, tmp_folder \\ nil) do 50 | log = 51 | capture_io(:stderr, fn -> 52 | Code.compile_string(str) 53 | {:module, ^module} = Code.ensure_loaded(module) 54 | end) 55 | 56 | if log != "" do 57 | Logger.info("compiler warning compiling Elixir: #{log}") 58 | end 59 | 60 | io = 61 | capture_io(fn -> 62 | Enum.each(input, &send(self(), {:input, elem(&1, 1)})) 63 | apply(module, :main, []) 64 | end) 65 | 66 | true = :code.delete(module) 67 | :code.purge(module) 68 | 69 | files = 70 | case tmp_folder do 71 | nil -> nil 72 | _ -> Util.get_new_files([], File.ls!(tmp_folder), tmp_folder) 73 | end 74 | 75 | %{output: io, files: files} 76 | end 77 | 78 | @doc """ 79 | This is the one-stop-shop for ensuring cobol text acts the same as the converted elixir version. 80 | Given cobol text, a module name, expected output, and a list of input, it will 81 | 1) Compile and execute the cobol code, sending the specified inputs 82 | 2) Run CobolToElixir and convert the cobol code to Elixir 83 | 3) Load and run the Elixir code, sending the specified inputs 84 | 4) Assert that the output of both the cobol and Elixir programs matches the specififed output 85 | """ 86 | def assert_output_equal(cobol_text, module, opts \\ []) when is_list(opts) do 87 | output = Keyword.get(opts, :output, "") 88 | input = Keyword.get(opts, :input, []) 89 | %{output: cobol_output, files: cobol_files} = execute_cobol_code!(cobol_text, input) 90 | 91 | if !is_nil(output) do 92 | ExUnit.Assertions.assert(cobol_output == output) 93 | end 94 | 95 | tmp_folder = Util.generate_tmp_folder() 96 | {:ok, elixir_text} = CobolToElixir.convert(cobol_text, accept_via_message: true, io_dir: tmp_folder) 97 | 98 | try do 99 | %{output: elixir_output, files: elixir_files} = execute_elixir_code(elixir_text, module, input, tmp_folder) 100 | 101 | ExUnit.Assertions.assert(cobol_output == elixir_output) 102 | ExUnit.Assertions.assert(cobol_files == elixir_files) 103 | after 104 | File.rm_rf!(tmp_folder) 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, 4 | "ex_doc": {:hex, :ex_doc, "0.25.1", "4b736fa38dc76488a937e5ef2944f5474f3eff921de771b25371345a8dc810bc", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3200b0a69ddb2028365281fbef3753ea9e728683863d8cdaa96580925c891f67"}, 5 | "excoveralls": {:hex, :excoveralls, "0.14.0", "4b562d2acd87def01a3d1621e40037fdbf99f495ed3a8570dfcf1ab24e15f76d", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "94f17478b0cca020bcd85ce7eafea82d2856f7ed022be777734a2f864d36091a"}, 6 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 7 | "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, 8 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 9 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 10 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 13 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 14 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 15 | "mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 17 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 18 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 19 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 20 | } 21 | -------------------------------------------------------------------------------- /livebook_examples.livemd: -------------------------------------------------------------------------------- 1 | # CobolToElixir 2 | 3 | ## Installing CobolToElixir 4 | 5 | COBOL is not technically necessary to run CobolToElixir, but the examples below do use it to verify expected output. You can install it with `brew install gnu-cobol` (this may take a while, run outside of Livebook instead to see progress). 6 | 7 | ```elixir 8 | {_, 0} = System.cmd("brew", ["install", "gnu-cobol"], stderr_to_stdout: true) 9 | ``` 10 | 11 | Verify it installed correctly by running the COBOL Compiler `cobc`: 12 | 13 | ```elixir 14 | {"GnuCOBOL" <> _, 0} = System.cmd("cobc", ["-h"], stderr_to_stdout: true) 15 | ``` 16 | 17 | Next, add `:cobol_to_elixir`. In a project, add this line to your dependencies: 18 | 19 | 20 | 21 | ```elixir 22 | {:cobol_to_elixir, "~> 0.0.0"} 23 | ``` 24 | 25 | In this Livebook, we can install using `Mix.install` 26 | 27 | ```elixir 28 | :ok = 29 | Mix.install([ 30 | {:cobol_to_elixir, 31 | git: "git@github.com:TheFirstAvenger/cobol_to_elixir.git", 32 | ref: "dc082c1e22ed06019fa1da777f53658cf81f0b3b"} 33 | ]) 34 | ``` 35 | 36 | ## Convert a simple COBOL program 37 | 38 | Lets start with a simple COBOL program to Elixir. This program has one variable set, and displays that variable: 39 | 40 | ```elixir 41 | cobol = """ 42 | >>SOURCE FORMAT FREE 43 | IDENTIFICATION DIVISION. 44 | PROGRAM-ID. Livebook1. 45 | AUTHOR. Mike Binns. 46 | DATE-WRITTEN. June 25th 2021 47 | DATA DIVISION. 48 | WORKING-STORAGE SECTION. 49 | 01 Name PIC X(4) VALUE "Mike". 50 | PROCEDURE DIVISION. 51 | 52 | DISPLAY "Hello " Name 53 | 54 | STOP RUN. 55 | """ 56 | ``` 57 | 58 | Now, lets validate that this is correct COBOL by compiling and then executing it using `CobolToElixir.Util.execute_cobol_code!/2` 59 | 60 | ```elixir 61 | %{output: "Hello Mike\n"} = CobolToElixir.Util.execute_cobol_code!(cobol) 62 | ``` 63 | 64 | Next, let's use `CobolToElixir.convert()` to convert the COBOL code to Elixir: 65 | 66 | ```elixir 67 | elixir_code = CobolToElixir.convert!(cobol) 68 | IO.puts(elixir_code) 69 | ``` 70 | 71 | There is a bunch of boilerplate helper functions, but notice that the Module is named `ElixirFromCobol.Livebook1`, the author and date written are added to the @moduledoc, and that the `do_main` function contains the Elixir version of our COBOL working storage section and procedure division: 72 | 73 | 74 | 75 | ```elixir 76 | defmodule ElixirFromCobol.Livebook1 do 77 | @moduledoc """ 78 | author: Mike Binns 79 | date written: June 25th 2021 80 | """ 81 | 82 | ... 83 | 84 | def do_main do 85 | # pic: XXXX 86 | var_Name = "Mike" 87 | pics = %{"Name" => {:str, "XXXX", 4}} 88 | IO.puts "Hello " <> var_Name 89 | throw :stop_run 90 | end 91 | ``` 92 | 93 | Next, lets try running that Elixir code. We will define a local function to compile the give Elixir code, load it into memory, run the `main` function, and then unload the code from memory. We will then run that function with the Elixir code output from our CobolToElixir transpiler. 94 | 95 | ```elixir 96 | execute_elixir = fn elixir_code, module -> 97 | Code.compile_string(elixir_code) 98 | {:module, ^module} = Code.ensure_loaded(module) 99 | 100 | apply(module, :main, []) 101 | 102 | true = :code.delete(module) 103 | :code.purge(module) 104 | :ok 105 | end 106 | 107 | execute_elixir.(elixir_code, ElixirFromCobol.Livebook1) 108 | ``` 109 | 110 | Note that there was a compiler warning because we defined `pics` but did not use it. More advanced code conversions will use this variable. 111 | 112 | Also notice that `Hello Mike` appears in the logs. 113 | 114 | ## Additional Examples - Paragraphs 115 | 116 | COBOL uses "paragraphs", which are similar to functions, but not exactly the same. Below we can see how a more complex COBOL program, with multiple paragraphs calling into each other, is converted to Elixir. 117 | 118 | First, lets define the Cobol code 119 | 120 | ```elixir 121 | cobol = """ 122 | >>SOURCE FORMAT FREE 123 | IDENTIFICATION DIVISION. 124 | PROGRAM-ID. proceduretest. 125 | AUTHOR. Mike Binns. 126 | DATE-WRITTEN.March 19th 2021. 127 | PROCEDURE DIVISION. 128 | SubOne. 129 | DISPLAY "In Paragraph 1" 130 | PERFORM SubTwo 131 | DISPLAY "Returned to Paragraph 1" 132 | PERFORM SubFour 2 TIMES. 133 | STOP RUN. 134 | 135 | SubThree. 136 | DISPLAY "In Paragraph 3". 137 | 138 | SubTwo. 139 | DISPLAY "In Paragraph 2" 140 | PERFORM SubThree 141 | DISPLAY "Returned to Paragraph 2". 142 | 143 | SubFour. 144 | DISPLAY "Repeat". 145 | 146 | STOP RUN. 147 | 148 | """ 149 | ``` 150 | 151 | Next, we execute that COBOL code to verify it is valid, and to determine the expected output 152 | 153 | ```elixir 154 | %{output: cobol_output} = CobolToElixir.Util.execute_cobol_code!(cobol) 155 | IO.puts(cobol_output) 156 | ``` 157 | 158 | Now, lets convert that COBOL to Elixir. Note the contents of the `do_main` function, and the other functions that were created to mirror the paragraphs. 159 | 160 | ```elixir 161 | elixir_code = CobolToElixir.convert!(cobol) 162 | IO.puts(elixir_code) 163 | ``` 164 | 165 | And finally, run that Elixir code and see that the output is the same as the COBOL output 166 | 167 | ```elixir 168 | IO.puts("Elixir output:") 169 | execute_elixir.(elixir_code, ElixirFromCobol.Proceduretest) 170 | IO.puts("Cobol output:") 171 | IO.puts(cobol_output) 172 | ``` 173 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/data_division/group_items_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.DataDivision.GroupItemsTest do 2 | use CobolToElixirCase 3 | 4 | alias CobolToElixir.Parsed 5 | alias CobolToElixir.Parser 6 | alias CobolToElixir.Parsed.Variable 7 | alias CobolToElixir.Tokenizer 8 | 9 | test "Parses a group item" do 10 | cobol = """ 11 | >>SOURCE FORMAT FREE 12 | IDENTIFICATION DIVISION. 13 | PROGRAM-ID. test1. 14 | DATA DIVISION. 15 | WORKING-STORAGE SECTION. 16 | 01 Customer. 17 | 02 IDENT PIC 9(3). 18 | 02 CustName PIC X(5). 19 | 02 DateOfBirth. 20 | 03 MOB PIC 99. 21 | 03 DOB PIC 99. 22 | 03 YOB PIC 9(4). 23 | """ 24 | 25 | validate_cobol_code(cobol) 26 | 27 | {:ok, tokenized} = Tokenizer.tokenize(cobol) 28 | {:ok, parsed} = Parser.parse(tokenized) 29 | 30 | assert %Parsed{ 31 | variables: [ 32 | %Variable{ 33 | name: "Customer", 34 | type: :map, 35 | value: %{ 36 | "IDENT" => 0, 37 | "CustName" => " ", 38 | "DateOfBirth" => %{ 39 | "MOB" => 0, 40 | "DOB" => 0, 41 | "YOB" => 0 42 | } 43 | } 44 | } 45 | ], 46 | variable_map: %{ 47 | "Customer" => %Variable{name: "Customer", type: :map}, 48 | "IDENT" => %Variable{name: "IDENT", type: :map_child, value: ["Customer"]}, 49 | "CustName" => %Variable{name: "CustName", type: :map_child, value: ["Customer"]}, 50 | "DateOfBirth" => %Variable{name: "DateOfBirth", type: :map_child_map, value: ["Customer"]}, 51 | "MOB" => %Variable{name: "MOB", type: :map_child, value: ["Customer", "DateOfBirth"]}, 52 | "DOB" => %Variable{name: "DOB", type: :map_child, value: ["Customer", "DateOfBirth"]}, 53 | "YOB" => %Variable{name: "YOB", type: :map_child, value: ["Customer", "DateOfBirth"]} 54 | } 55 | } = parsed 56 | end 57 | 58 | test "renders a group item" do 59 | cobol = """ 60 | >>SOURCE FORMAT FREE 61 | IDENTIFICATION DIVISION. 62 | PROGRAM-ID. test1. 63 | DATA DIVISION. 64 | WORKING-STORAGE SECTION. 65 | 01 Customer. 66 | 02 IDENT PIC 9(3). 67 | 02 CustName PIC X(5). 68 | 02 DateOfBirth. 69 | 03 MOB PIC 99. 70 | 03 DOB PIC 99. 71 | 03 YOB PIC 9(4). 72 | """ 73 | 74 | validate_cobol_code(cobol) 75 | 76 | elixir = CobolToElixir.convert!(cobol) 77 | 78 | assert elixir =~ 79 | ~s|var_Customer = %{"CustName" => " ", "DateOfBirth" => %{"DOB" => 0, "MOB" => 0, "YOB" => 0}, "IDENT" => 0}| 80 | end 81 | 82 | test "renders a group item followed by a single item" do 83 | cobol = """ 84 | >>SOURCE FORMAT FREE 85 | IDENTIFICATION DIVISION. 86 | PROGRAM-ID. test1. 87 | DATA DIVISION. 88 | WORKING-STORAGE SECTION. 89 | 01 Customer. 90 | 02 IDENT PIC 9(3). 91 | 02 CustName PIC X(5). 92 | 02 DateOfBirth. 93 | 03 MOB PIC 99. 94 | 03 DOB PIC 99. 95 | 03 YOB PIC 9(4). 96 | 02 Title PIC X(4). 97 | 01 Note PIC X(4). 98 | """ 99 | 100 | validate_cobol_code(cobol) 101 | 102 | elixir = CobolToElixir.convert!(cobol) 103 | 104 | assert elixir =~ 105 | ~s|var_Customer = %{"CustName" => " ", "DateOfBirth" => | <> 106 | ~s|%{"DOB" => 0, "MOB" => 0, "YOB" => 0}, "IDENT" => 0, "Title" => " "}| 107 | 108 | assert elixir =~ ~s|var_Note = " "| 109 | end 110 | 111 | test "displays a group item" do 112 | cobol = """ 113 | >>SOURCE FORMAT FREE 114 | IDENTIFICATION DIVISION. 115 | PROGRAM-ID. test1. 116 | DATA DIVISION. 117 | WORKING-STORAGE SECTION. 118 | 01 Customer. 119 | 02 IDENT PIC 9(3) VALUE 042. 120 | 02 CustName PIC X(5) VALUE "MIKEB". 121 | 02 DateOfBirth. 122 | 03 MOB PIC 99 VALUE 1. 123 | 03 DOB PIC 99 VALUE 2. 124 | 03 YOB PIC 9(4) VALUE 1982. 125 | 02 Title PIC X(3) VALUE "SSE". 126 | PROCEDURE DIVISION. 127 | DISPLAY "Customer: " Customer. 128 | """ 129 | 130 | expected_output = "Customer: 042MIKEB01021982SSE\n" 131 | 132 | assert_output_equal(cobol, ElixirFromCobol.Test1, output: expected_output) 133 | end 134 | 135 | test "renders a move into a map child" do 136 | cobol = """ 137 | >>SOURCE FORMAT FREE 138 | IDENTIFICATION DIVISION. 139 | PROGRAM-ID. test1. 140 | DATA DIVISION. 141 | WORKING-STORAGE SECTION. 142 | 01 Customer. 143 | 02 IDENT PIC 9(3) VALUE 042. 144 | 02 CustName PIC X(5) VALUE "MIKEB". 145 | 02 DateOfBirth. 146 | 03 MOB PIC 99 VALUE 1. 147 | 03 DOB PIC 99 VALUE 2. 148 | 03 YOB PIC 9(4) VALUE 1982. 149 | 02 Title PIC X(3) VALUE "SSE". 150 | PROCEDURE DIVISION. 151 | DISPLAY "Customer: " Customer. 152 | MOVE "JOHND" TO CustName 153 | MOVE 1945 TO YOB 154 | DISPLAY "Customer: " Customer. 155 | """ 156 | 157 | validate_cobol_code(cobol) 158 | 159 | expected_output = "Customer: 042MIKEB01021982SSE\nCustomer: 042JOHND01021945SSE\n" 160 | 161 | assert_output_equal(cobol, ElixirFromCobol.Test1, output: expected_output) 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /test/cobol_to_elixir/data_division_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.DataDivisionTest do 2 | use CobolToElixirCase 3 | 4 | test "handles no DATA section" do 5 | cobol = """ 6 | >>SOURCE FORMAT FREE 7 | IDENTIFICATION DIVISION. 8 | PROGRAM-ID. test1. 9 | """ 10 | 11 | validate_cobol_code(cobol) 12 | 13 | refute CobolToElixir.convert!(cobol) =~ ~s|var_| 14 | end 15 | 16 | test "raises on bad variable value" do 17 | cobol = """ 18 | >>SOURCE FORMAT FREE 19 | IDENTIFICATION DIVISION. 20 | PROGRAM-ID. test1. 21 | DATA DIVISION. 22 | WORKING-STORAGE SECTION. 23 | 01 NAME PIC X(5) VALUE NO_QUOTES. 24 | """ 25 | 26 | assert_raise RuntimeError, "String variable NO_QUOTES did not start and end with quotes", fn -> 27 | CobolToElixir.convert!(cobol) 28 | end 29 | end 30 | 31 | describe "Simple String Variables" do 32 | test "Handles X(5) notation" do 33 | cobol = """ 34 | >>SOURCE FORMAT FREE 35 | IDENTIFICATION DIVISION. 36 | PROGRAM-ID. test1. 37 | DATA DIVISION. 38 | WORKING-STORAGE SECTION. 39 | 01 NAME PIC X(5). 40 | """ 41 | 42 | validate_cobol_code(cobol) 43 | 44 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: XXXXX\n var_NAME = " "| 45 | end 46 | 47 | test "Handles XXXXXX notation" do 48 | cobol = """ 49 | >>SOURCE FORMAT FREE 50 | IDENTIFICATION DIVISION. 51 | PROGRAM-ID. test1. 52 | DATA DIVISION. 53 | WORKING-STORAGE SECTION. 54 | 01 NAME PIC XXXXX. 55 | """ 56 | 57 | validate_cobol_code(cobol) 58 | 59 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: XXXXX\n var_NAME = " "| 60 | end 61 | 62 | test "Populates values" do 63 | cobol = """ 64 | >>SOURCE FORMAT FREE 65 | IDENTIFICATION DIVISION. 66 | PROGRAM-ID. test1. 67 | DATA DIVISION. 68 | WORKING-STORAGE SECTION. 69 | 01 NAME PIC XXXXX VALUE "Mikey". 70 | """ 71 | 72 | validate_cobol_code(cobol) 73 | 74 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: XXXXX\n var_NAME = "Mikey"| 75 | end 76 | 77 | test "Populates values with single quotes" do 78 | cobol = """ 79 | >>SOURCE FORMAT FREE 80 | IDENTIFICATION DIVISION. 81 | PROGRAM-ID. test1. 82 | DATA DIVISION. 83 | WORKING-STORAGE SECTION. 84 | 01 NAME PIC XXXXX VALUE 'Mikey'. 85 | """ 86 | 87 | validate_cobol_code(cobol) 88 | 89 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: XXXXX\n var_NAME = "Mikey"| 90 | end 91 | 92 | test "Populates spaces" do 93 | cobol = """ 94 | >>SOURCE FORMAT FREE 95 | IDENTIFICATION DIVISION. 96 | PROGRAM-ID. test1. 97 | DATA DIVISION. 98 | WORKING-STORAGE SECTION. 99 | 01 NAME PIC XXXXX VALUE SPACES. 100 | """ 101 | 102 | validate_cobol_code(cobol) 103 | 104 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: XXXXX\n var_NAME = " "| 105 | end 106 | 107 | test "Trims values" do 108 | cobol = """ 109 | >>SOURCE FORMAT FREE 110 | IDENTIFICATION DIVISION. 111 | PROGRAM-ID. test1. 112 | DATA DIVISION. 113 | WORKING-STORAGE SECTION. 114 | 01 NAME PIC XXXX VALUE "Mikey". 115 | """ 116 | 117 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: XXXX\n var_NAME = "Mike"| 118 | end 119 | 120 | test "Pads values" do 121 | cobol = """ 122 | >>SOURCE FORMAT FREE 123 | IDENTIFICATION DIVISION. 124 | PROGRAM-ID. test1. 125 | DATA DIVISION. 126 | WORKING-STORAGE SECTION. 127 | 01 NAME PIC XXXXXX VALUE "Mikey". 128 | """ 129 | 130 | validate_cobol_code(cobol) 131 | 132 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: XXXXXX\n var_NAME = "Mikey "| 133 | end 134 | end 135 | 136 | describe "Simple numeric values" do 137 | test "Handles 9(4) notation" do 138 | cobol = """ 139 | >>SOURCE FORMAT FREE 140 | IDENTIFICATION DIVISION. 141 | PROGRAM-ID. test1. 142 | DATA DIVISION. 143 | WORKING-STORAGE SECTION. 144 | 01 YEAR PIC 9(4). 145 | """ 146 | 147 | validate_cobol_code(cobol) 148 | 149 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: 9999\n var_YEAR = 0| 150 | end 151 | 152 | test "Handles 9999 notation" do 153 | cobol = """ 154 | >>SOURCE FORMAT FREE 155 | IDENTIFICATION DIVISION. 156 | PROGRAM-ID. test1. 157 | DATA DIVISION. 158 | WORKING-STORAGE SECTION. 159 | 01 YEAR PIC 9999. 160 | """ 161 | 162 | validate_cobol_code(cobol) 163 | 164 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: 9999\n var_YEAR = 0| 165 | end 166 | 167 | test "Populates values" do 168 | cobol = """ 169 | >>SOURCE FORMAT FREE 170 | IDENTIFICATION DIVISION. 171 | PROGRAM-ID. test1. 172 | DATA DIVISION. 173 | WORKING-STORAGE SECTION. 174 | 01 YEAR PIC 9999 VALUE 1234. 175 | """ 176 | 177 | validate_cobol_code(cobol) 178 | 179 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: 9999\n var_YEAR = 1234| 180 | end 181 | 182 | test "Populates zeroes" do 183 | cobol = """ 184 | >>SOURCE FORMAT FREE 185 | IDENTIFICATION DIVISION. 186 | PROGRAM-ID. test1. 187 | DATA DIVISION. 188 | WORKING-STORAGE SECTION. 189 | 01 YEAR PIC 9999 VALUE ZEROS. 190 | """ 191 | 192 | validate_cobol_code(cobol) 193 | 194 | assert CobolToElixir.convert!(cobol) =~ ~s|# pic: 9999\n var_YEAR = 0| 195 | end 196 | 197 | test "Raises on overflow" do 198 | cobol = """ 199 | >>SOURCE FORMAT FREE 200 | IDENTIFICATION DIVISION. 201 | PROGRAM-ID. test1. 202 | DATA DIVISION. 203 | WORKING-STORAGE SECTION. 204 | 01 YEAR PIC 9999 VALUE 12345. 205 | """ 206 | 207 | assert_raise RuntimeError, "Variable YEAR has value 12345 which is larger than pic (9999) allows", fn -> 208 | CobolToElixir.convert!(cobol) 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/cobol_to_elixir/tokenizer.ex: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.Tokenizer do 2 | require Logger 3 | @integers_as_strings ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] 4 | @two_integers_as_strings for a <- @integers_as_strings, 5 | b <- @integers_as_strings, 6 | do: a <> b 7 | 8 | def tokenize(program) do 9 | tokens = 10 | program 11 | |> String.split("\n") 12 | |> Enum.map(&String.trim(&1, " ")) 13 | |> Enum.map(&String.trim_trailing(&1, ".")) 14 | |> Enum.reject(&(&1 == "")) 15 | |> Enum.map(&tokenize_line/1) 16 | 17 | {:ok, tokens} 18 | end 19 | 20 | def tokenize_line("*>" <> comment), do: {:comment, String.trim_leading(comment, " ")} 21 | def tokenize_line("*" <> comment), do: {:comment, String.trim_leading(comment, " ")} 22 | def tokenize_line(">>SOURCE FORMAT FREE"), do: :source_format_free 23 | def tokenize_line(~s|$ SET SOURCEFORMAT"FREE"|), do: :source_format_free 24 | 25 | def tokenize_line(line) do 26 | cond do 27 | division = match_division(line) -> {:division, division} 28 | section = match_section(line) -> {:section, section} 29 | variable_line = match_variable_line(line) -> {:variable_line, variable_line} 30 | move_line = match_move_line(line) -> {:move_line, move_line} 31 | simple = match_simple(line) -> simple 32 | complex = match_complex(String.split(line)) -> complex 33 | paragraph = match_paragraph(line) -> {:paragraph, paragraph} 34 | true -> {:not_tokenized, warn_not_tokenized(line)} 35 | end 36 | end 37 | 38 | defp warn_not_tokenized(line) do 39 | Logger.warn("Unable to tokenize line: #{line}") 40 | line 41 | end 42 | 43 | defp match_division(line) do 44 | case String.split(line, " ") do 45 | [division, "DIVISION"] -> division 46 | _ -> false 47 | end 48 | end 49 | 50 | defp match_section(line) do 51 | case String.split(line, " ") do 52 | [section, "SECTION"] -> section 53 | _ -> false 54 | end 55 | end 56 | 57 | def match_variable_line(line) do 58 | case split_vars_and_strings(line) do 59 | [level, variable] when level in @two_integers_as_strings -> 60 | [tokenize_level(level), variable] 61 | 62 | [level, variable, "PIC", picture_clause] when level in @two_integers_as_strings -> 63 | [tokenize_level(level), variable, {:pic, picture_clause}] 64 | 65 | [level, variable, "PIC", picture_clause, "VALUE", value] when level in @two_integers_as_strings -> 66 | [tokenize_level(level), variable, {:pic, picture_clause}, {:value, maybe_to_zeros_or_spaces(value)}] 67 | 68 | [level, variable, "CONSTANT", "AS", value] when level in @two_integers_as_strings -> 69 | [tokenize_level(level), variable, :constant, {:value, value}] 70 | 71 | _ -> 72 | false 73 | end 74 | end 75 | 76 | defp tokenize_level(level) do 77 | {:level, String.to_integer(level)} 78 | end 79 | 80 | defp match_paragraph(line) do 81 | if Regex.match?(~r/^[a-z\-A-Z0-9]+$/, line) do 82 | line 83 | else 84 | false 85 | end 86 | end 87 | 88 | defp maybe_to_zeros_or_spaces("ZERO"), do: :zeros 89 | defp maybe_to_zeros_or_spaces("ZEROS"), do: :zeros 90 | defp maybe_to_zeros_or_spaces("SPACE"), do: :spaces 91 | defp maybe_to_zeros_or_spaces("SPACES"), do: :spaces 92 | defp maybe_to_zeros_or_spaces(other), do: other 93 | 94 | def match_move_line("MOVE " <> move_line), do: split_vars_and_strings(move_line) 95 | def match_move_line(_), do: false 96 | 97 | defp split_vars_and_strings(line) do 98 | ~r("[^"]*"|'[^']*'|[^"\s]+) 99 | |> Regex.scan(line) 100 | |> List.flatten() 101 | |> Enum.map(&single_quotes_to_double/1) 102 | end 103 | 104 | defp single_quotes_to_double("'" <> rest) do 105 | ~s|"#{String.trim_trailing(rest, "'")}"| 106 | end 107 | 108 | defp single_quotes_to_double(other), do: other 109 | 110 | def to_vars_and_strings(["\"" <> string | tail]), 111 | do: [{:string, String.trim_trailing(string, "\"")} | to_vars_and_strings(tail)] 112 | 113 | def to_vars_and_strings(["'" <> string | tail]), 114 | do: [{:string, String.trim_trailing(string, "'")} | to_vars_and_strings(tail)] 115 | 116 | def to_vars_and_strings([var | tail]), do: [{:variable, var} | to_vars_and_strings(tail)] 117 | def to_vars_and_strings([]), do: [] 118 | 119 | defp match_simple("PROGRAM-ID." <> id), do: {:program_id, String.trim(id)} 120 | defp match_simple("AUTHOR." <> author), do: {:author, String.trim(author)} 121 | defp match_simple("DATE-WRITTEN." <> date), do: {:date_written, String.trim(date)} 122 | defp match_simple("DISPLAY " <> display), do: parse_display(display) 123 | defp match_simple("ACCEPT " <> accept), do: {:accept, accept} 124 | defp match_simple("COMPUTE " <> compute), do: {:compute, compute} 125 | defp match_simple("PERFORM " <> perform), do: {:perform, parse_perform(perform)} 126 | defp match_simple("STOP RUN"), do: {:stop, :run} 127 | defp match_simple("FILE-CONTROL"), do: :file_control 128 | defp match_simple("ORGANIZATION IS LINE SEQUENTIAL"), do: {:organization, :line_sequential} 129 | defp match_simple("ACCESS IS SEQUENTIAL"), do: {:access, :sequential} 130 | defp match_simple("END-WRITE"), do: :end_write 131 | defp match_simple(_), do: false 132 | 133 | defp match_complex(["FD", file_descriptor_name]), do: {:fd, file_descriptor_name} 134 | defp match_complex(["OPEN", "OUTPUT", file_descriptor_name]), do: {:open, :output, file_descriptor_name} 135 | defp match_complex(["WRITE", file_descriptor_name]), do: {:write, file_descriptor_name} 136 | defp match_complex(["CLOSE", file_descriptor_name]), do: {:close, file_descriptor_name} 137 | 138 | defp match_complex(["SELECT", file_var_name, "ASSIGN", "TO", file_name]), 139 | do: {:select, file_var_name, :assign, :to, file_name} 140 | 141 | defp match_complex(_), do: false 142 | 143 | defp parse_perform(perform) do 144 | case String.split(perform, " ") do 145 | [name] -> {:repeat, 1, name} 146 | [name, x, "TIMES"] -> {:repeat, String.to_integer(x), name} 147 | end 148 | end 149 | 150 | defp parse_display(display) do 151 | {advancing, display} = do_parse_display([], split_vars_and_strings(display)) 152 | 153 | if advancing do 154 | {:display, to_vars_and_strings(display)} 155 | else 156 | {:display_no_advancing, to_vars_and_strings(display)} 157 | end 158 | end 159 | 160 | defp do_parse_display(acc, ["WITH", "NO", "ADVANCING"]), do: {false, Enum.reverse(acc)} 161 | defp do_parse_display(acc, [a | rest]), do: do_parse_display([a | acc], rest) 162 | defp do_parse_display(acc, []), do: {true, Enum.reverse(acc)} 163 | end 164 | -------------------------------------------------------------------------------- /lib/cobol_to_elixir/elixirizer.ex: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.Elixirizer do 2 | alias CobolToElixir.Parsed 3 | alias CobolToElixir.Parsed.Variable 4 | 5 | require Logger 6 | 7 | def elixirize(%Parsed{program_id: program_id} = parsed, opts \\ []) do 8 | namespace = Keyword.get(opts, :namespace, "ElixirFromCobol") 9 | io_dir = Keyword.get(opts, :io_dir, "") 10 | accept_via_message = Keyword.get(opts, :accept_via_message, false) 11 | module_name = program_id_to_module_name(program_id) 12 | 13 | lines = 14 | [ 15 | ~s|defmodule #{namespace}.#{module_name} do|, 16 | ~s| @moduledoc """|, 17 | ~s| author: #{parsed.author || "n/a"}|, 18 | ~s| date written: #{parsed.date_written || "n/a"}|, 19 | ~s| """|, 20 | ~s||, 21 | ~s| @io_dir #{inspect(io_dir)}|, 22 | ~s||, 23 | ~s| def main do|, 24 | ~s| try do|, 25 | ~s| do_main()|, 26 | ~s| catch|, 27 | ~s| :stop_run -> :stop_run|, 28 | ~s| end|, 29 | ~s| end|, 30 | ~s|| 31 | ] ++ 32 | do_main_function(parsed) ++ 33 | do_paragraphs(parsed) ++ 34 | [ 35 | ~s||, 36 | ~s| defp do_accept() do| 37 | ] ++ 38 | accept_lines(accept_via_message) ++ 39 | [ 40 | ~s| end|, 41 | ~s||, 42 | ~s/ def accept({:str, _, _} = pic), do: do_accept() |> format(pic)/, 43 | ~s||, 44 | ~s| def accept(_), do: do_accept()|, 45 | ~s||, 46 | ~s/ def format(str, {:str, _, length}), do: str |> String.slice(0..(length - 1)) |> String.pad_trailing(length)/, 47 | ~s/ def format(int, {:int, _, length}), do: "\#{int}" |> String.slice(0..(length - 1)) |> String.pad_leading(length, "0")/, 48 | ~s||, 49 | ~s| def display_group_item(group_item = %{}, children_paths, pics) do|, 50 | ~s| children_paths|, 51 | ~s/ |> Enum.reduce([], fn path, values ->/, 52 | ~s| name = List.last(path)|, 53 | ~s| pic = pics[name]|, 54 | ~s| val = get_in(group_item, tl(path))|, 55 | ~s/ [format(val, pic) | values]/, 56 | ~s| end)|, 57 | ~s/ |> Enum.reverse()/, 58 | ~s/ |> Enum.join("")/, 59 | ~s| end|, 60 | ~s||, 61 | ~s|end| 62 | ] 63 | 64 | {:ok, Enum.join(lines, "\n")} 65 | end 66 | 67 | defp accept_lines(false), do: [~s/ "" |> IO.gets() |> String.trim_trailing("\\n")/] 68 | 69 | defp accept_lines(true) do 70 | [ 71 | ~s/ receive do/, 72 | ~s/ {:input, input} -> input/, 73 | ~s/ end/ 74 | ] 75 | end 76 | 77 | defp program_id_to_module_name(program_id) do 78 | String.capitalize(program_id) 79 | end 80 | 81 | def do_main_function(%Parsed{} = parsed) do 82 | [ 83 | ~s| def do_main do| 84 | ] ++ 85 | variables(parsed) ++ 86 | pics(parsed) ++ 87 | procedure(parsed) ++ 88 | [ 89 | ~s| end| 90 | ] 91 | end 92 | 93 | def do_paragraphs(%Parsed{paragraphs: paragraphs} = parsed) do 94 | Enum.flat_map(paragraphs, &do_paragraph(&1, parsed)) 95 | end 96 | 97 | defp do_paragraph({name, lines}, parsed) do 98 | [~s||, ~s| def paragraph_#{name} do|] ++ procedure(%Parsed{parsed | procedure: lines}) ++ [~s| end|] 99 | end 100 | 101 | def variables(%Parsed{variables: variables}) do 102 | Enum.flat_map(variables, &variable_to_line/1) 103 | end 104 | 105 | def pics(%Parsed{variables: []}), do: [] 106 | 107 | def pics(%Parsed{variable_map: variable_map}) do 108 | pics = 109 | variable_map 110 | |> Enum.map(fn {name, %Variable{pic: pic}} -> {name, pic} end) 111 | |> Enum.reject(&is_nil(elem(&1, 1))) 112 | |> Enum.into(%{}) 113 | 114 | [~s| pics = #{inspect(pics)}|] 115 | end 116 | 117 | def procedure(%Parsed{procedure: procedure} = parsed) do 118 | Enum.flat_map(procedure, &procedure_to_line(&1, parsed)) 119 | end 120 | 121 | def variable_to_line(%Variable{type: :map, value: value, children: children} = variable) do 122 | [ 123 | ~s| #{variable_name(variable)} = #{inspect(value)}|, 124 | ~s| #{group_child_paths_name(variable)} = #{inspect(children)}| 125 | ] 126 | end 127 | 128 | def variable_to_line(%Variable{type: :single, pic: pic, value: value} = variable) do 129 | [ 130 | ~s| # pic: #{pic_str(pic)}|, 131 | ~s| #{variable_name(variable)} = #{maybe_parens(value, pic)}| 132 | ] 133 | end 134 | 135 | defp maybe_parens(val, {:str, _, _}), do: ~s|"#{val}"| 136 | defp maybe_parens(val, _), do: val 137 | 138 | defp pic_str(nil), do: "none" 139 | defp pic_str(pic), do: elem(pic, 1) 140 | 141 | defp variable_name(%Variable{name: name}), do: variable_name(name) 142 | defp variable_name(name), do: "var_#{name}" 143 | 144 | defp group_child_paths_name(%Variable{type: :map, name: name}), do: group_child_paths_name(name) 145 | defp group_child_paths_name(name) when is_binary(name), do: "child_paths_of_#{name}" 146 | 147 | defp procedure_to_line({display_type, display}, %Parsed{} = parsed) 148 | when display_type in [:display_no_advancing, :display] do 149 | io_type = 150 | case display_type do 151 | :display -> "puts" 152 | :display_no_advancing -> "write" 153 | end 154 | 155 | to_display = 156 | display 157 | |> display_vars_and_strings(parsed) 158 | |> Enum.join(" <> ") 159 | 160 | [~s| IO.#{io_type} #{to_display}|] 161 | end 162 | 163 | defp procedure_to_line({:accept, var}, %Parsed{variable_map: m}), 164 | do: [~s| #{variable_name(m[var])} = accept(#{inspect(m[var].pic)})|] 165 | 166 | defp procedure_to_line({:stop, :run}, _), do: [~s| throw :stop_run|] 167 | 168 | defp procedure_to_line({:move_line, commands}, %Parsed{variable_map: m}) do 169 | {var, from} = 170 | commands 171 | |> Enum.reverse() 172 | |> then(fn [var, "TO" | rest] -> 173 | {var, Enum.reverse(rest)} 174 | end) 175 | 176 | val = 177 | case from do 178 | [v] -> v 179 | _ -> raise "Move from something other than a single value not currently supported" 180 | end 181 | 182 | case Map.fetch!(m, var) do 183 | %Variable{type: :single} -> 184 | [~s| #{variable_name(var)} = format(#{val}, #{inspect(m[var].pic)})|] 185 | 186 | %Variable{type: :map_child, value: child_path} -> 187 | path = tl(child_path) ++ [var] 188 | 189 | [ 190 | ~s| #{variable_name(hd(child_path))} = put_in(#{variable_name(hd(child_path))}, #{inspect(path)}, format(#{val}, #{inspect(m[var].pic)}))| 191 | ] 192 | end 193 | end 194 | 195 | defp procedure_to_line({:perform_paragraph, paragraph_name}, _), 196 | do: [~s| paragraph_#{paragraph_name}()|] 197 | 198 | defp procedure_to_line({:perform, {:repeat, x, paragraph_name}}, _), 199 | do: List.duplicate(~s| paragraph_#{paragraph_name}()|, x) 200 | 201 | defp procedure_to_line({:open, :output, file_var_name}, %Parsed{} = parsed) do 202 | %{file_name: file_name} = Enum.find(parsed.file_control, &(&1.var_name == file_var_name)) 203 | {file_variables, _file_variable_map} = parsed.file_variables[file_var_name] 204 | [~s| current_file = #{file_name}| | Enum.flat_map(file_variables, &variable_to_line/1)] 205 | end 206 | 207 | defp procedure_to_line({:write, variable}, _) do 208 | [ 209 | ~s||, 210 | ~s| str_to_write = #{variable_name(variable)}|, 211 | ~s/ |> display_group_item(#{group_child_paths_name(variable)}, pics)/, 212 | ~s/ |> String.trim_trailing()/, 213 | ~s/ |> Kernel.<>("\\n")/, 214 | ~s||, 215 | ~s| File.write!(Path.join(@io_dir, current_file), str_to_write)|, 216 | ~s|| 217 | ] 218 | end 219 | 220 | defp procedure_to_line(:end_write, _), do: [] 221 | defp procedure_to_line({:close, _}, _), do: [~s| current_file = nil|] 222 | 223 | defp procedure_to_line(other, _) do 224 | # coveralls-ignore-start 225 | Logger.warn("No procedure_to_line for #{inspect(other)}") 226 | [] 227 | # coveralls-ignore-end 228 | end 229 | 230 | defp display_vars_and_strings([{:variable, var} | rest], %Parsed{variable_map: variable_map} = parsed) do 231 | vars = 232 | case Map.get(variable_map, var) do 233 | %{type: :map} = variable -> 234 | ~s|display_group_item(#{variable_name(variable)}, #{group_child_paths_name(variable)}, pics)| 235 | 236 | %{type: :single} -> 237 | variable_name(var) 238 | end 239 | 240 | [vars | display_vars_and_strings(rest, parsed)] 241 | end 242 | 243 | # THIS HAS TO HANDLE A MAP 244 | defp display_vars_and_strings([{:variable, var} | rest], parsed) do 245 | [variable_name(var) | display_vars_and_strings(rest, parsed)] 246 | end 247 | 248 | defp display_vars_and_strings([{:string, str} | rest], parsed), 249 | do: [~s|"#{str}"| | display_vars_and_strings(rest, parsed)] 250 | 251 | defp display_vars_and_strings([], _parsed), do: [] 252 | end 253 | -------------------------------------------------------------------------------- /lib/cobol_to_elixir/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule CobolToElixir.Parser do 2 | alias CobolToElixir.Parsed 3 | alias CobolToElixir.Parsed.Variable 4 | 5 | require Logger 6 | 7 | def parse(tokenized) do 8 | divisions = parse_divisions(tokenized) 9 | 10 | parsed = 11 | Enum.reduce(divisions, %Parsed{}, fn {name, division}, parsed -> 12 | parse_division(name, division, parsed) 13 | end) 14 | 15 | {:ok, parsed} 16 | end 17 | 18 | def parse_divisions(tokenized) do 19 | do_parse_divisions(tokenized, nil, %{}) 20 | end 21 | 22 | defp do_parse_divisions([:source_format_free | tail], nil, %{}), 23 | do: do_parse_divisions(tail, nil, %{}) 24 | 25 | defp do_parse_divisions([{:division, division} | tail], _current, divisions), 26 | do: do_parse_divisions(tail, division, Map.put_new(divisions, division, [])) 27 | 28 | defp do_parse_divisions([other | tail], current, divisions), 29 | do: do_parse_divisions(tail, current, update_in(divisions, [current], &(&1 ++ [other]))) 30 | 31 | defp do_parse_divisions([], _, divisions), do: divisions 32 | 33 | def parse_sections(tokenized) do 34 | do_parse_sections(tokenized, nil, %{}) 35 | end 36 | 37 | defp do_parse_sections([{:section, section} | tail], _current, sections), 38 | do: do_parse_sections(tail, section, Map.put_new(sections, section, [])) 39 | 40 | defp do_parse_sections([other | tail], current, sections), 41 | do: do_parse_sections(tail, current, update_in(sections, [current], &(&1 ++ [other]))) 42 | 43 | defp do_parse_sections([], _, sections), do: sections 44 | 45 | defp parse_division("IDENTIFICATION", identification, %Parsed{} = parsed) do 46 | if is_nil(identification), do: raise("No identification division") 47 | {program_id, identification} = Keyword.pop!(identification, :program_id) 48 | {author, identification} = Keyword.pop(identification, :author) 49 | {date_written, identification} = Keyword.pop(identification, :date_written) 50 | 51 | # coveralls-ignore-start 52 | case identification do 53 | [] -> :ok 54 | [{:not_tokenized, _}] -> :ok 55 | _ -> Logger.warn("Unparsed identification: #{inspect(Keyword.keys(identification))}") 56 | end 57 | 58 | # coveralls-ignore-end 59 | 60 | %Parsed{ 61 | parsed 62 | | program_id: program_id, 63 | author: author, 64 | date_written: date_written 65 | } 66 | end 67 | 68 | defp parse_division("DATA", data, parsed) do 69 | data 70 | |> parse_sections() 71 | |> Enum.reduce(parsed, fn {name, section}, parsed -> 72 | parse_data_section(name, section, parsed) 73 | end) 74 | end 75 | 76 | defp parse_division("PROCEDURE", procedure, %Parsed{} = parsed) do 77 | {procedure, paragraphs, remaining} = 78 | Enum.reduce(procedure, {[], %{}, nil}, fn 79 | {:paragraph, paragraph_name}, {procedure, paragraphs, nil} -> 80 | if Map.has_key?(paragraphs, paragraph_name), 81 | do: raise("Multiple paragraphs found named #{paragraph_name}") 82 | 83 | {[{:perform_paragraph, paragraph_name} | procedure], paragraphs, {paragraph_name, []}} 84 | 85 | {:paragraph, new_paragraph_name}, {procedure, paragraphs, {paragraph_name, lines}} -> 86 | if Map.has_key?(paragraphs, new_paragraph_name), 87 | do: raise("Multiple paragraphs found named #{new_paragraph_name}") 88 | 89 | {[{:perform_paragraph, new_paragraph_name} | procedure], 90 | Map.put(paragraphs, paragraph_name, Enum.reverse(lines)), {new_paragraph_name, []}} 91 | 92 | line, {procedure, paragraphs, nil} -> 93 | {[line | procedure], paragraphs, nil} 94 | 95 | line, {procedure, paragraphs, {paragraph_name, lines}} -> 96 | {procedure, paragraphs, {paragraph_name, [line | lines]}} 97 | end) 98 | 99 | paragraphs = 100 | case remaining do 101 | nil -> paragraphs 102 | {paragraph_name, lines} -> Map.put(paragraphs, paragraph_name, Enum.reverse(lines)) 103 | end 104 | 105 | %Parsed{parsed | procedure: Enum.reverse(procedure), paragraphs: paragraphs} 106 | end 107 | 108 | defp parse_division("ENVIRONMENT", environment, %Parsed{} = parsed) do 109 | environment 110 | |> parse_sections() 111 | |> Enum.reduce(parsed, fn {name, section}, %Parsed{} = parsed -> 112 | parse_environment_section(name, section, parsed) 113 | end) 114 | end 115 | 116 | defp parse_division(name, _contents, %Parsed{} = parsed) do 117 | Logger.warn("No parser for division #{name}") 118 | parsed 119 | end 120 | 121 | defp parse_data_section("WORKING-STORAGE", contents, %Parsed{} = parsed) do 122 | {variables, variable_map} = parse_variables(contents) 123 | variable_map = Map.merge(parsed.variable_map || %{}, variable_map) 124 | %Parsed{parsed | variables: variables, variable_map: variable_map} 125 | end 126 | 127 | defp parse_data_section("FILE", contents, %Parsed{} = parsed) do 128 | file_variables = parse_individual_files(contents, nil, %{}) 129 | 130 | variable_map = 131 | file_variables 132 | |> Map.values() 133 | |> Enum.map(&elem(&1, 1)) 134 | |> Enum.reduce(%{}, &Map.merge/2) 135 | |> Map.merge(parsed.variable_map) 136 | 137 | %Parsed{parsed | file_variables: file_variables, variable_map: variable_map} 138 | end 139 | 140 | defp parse_environment_section("INPUT-OUTPUT", contents, %Parsed{} = parsed) do 141 | case hd(contents) do 142 | :file_control -> 143 | [{:select, file_var_name, :assign, :to, file_name} | tl] = tl(contents) 144 | 145 | if tl != [{:organization, :line_sequential}, {:access, :sequential}] do 146 | Logger.warn("file control options differ from assumed") 147 | end 148 | 149 | %Parsed{parsed | file_control: [%{var_name: file_var_name, file_name: file_name}]} 150 | end 151 | end 152 | 153 | defp parse_individual_files([{:fd, file_name} | tl], _current_file_name, files), 154 | do: parse_individual_files(tl, file_name, Map.put(files, file_name, [])) 155 | 156 | defp parse_individual_files([{:variable_line, _} = line | tl], current_file_name, files) do 157 | files = Map.put(files, current_file_name, [line | files[current_file_name]]) 158 | parse_individual_files(tl, current_file_name, files) 159 | end 160 | 161 | defp parse_individual_files([], _, files) do 162 | files 163 | |> Enum.map(fn {name, lines} -> {name, lines |> Enum.reverse() |> parse_variables()} end) 164 | |> Enum.into(%{}) 165 | end 166 | 167 | defp parse_variables(contents) do 168 | {variable_lines, other} = Enum.split_with(contents, &(elem(&1, 0) == :variable_line)) 169 | 170 | if other != [] do 171 | Logger.warn("Unknown lines in variable section: #{inspect(other)}") 172 | end 173 | 174 | all_variables = 175 | Enum.map(variable_lines, fn {:variable_line, line} -> 176 | variable_line_to_variable(line) 177 | end) 178 | 179 | {variables, variable_map, _depth_list} = 180 | Enum.reduce(all_variables, {[], %{}, []}, fn %Variable{depth: depth} = variable, 181 | {variables, variable_map, depth_list} -> 182 | depth_list = prune_depth_list(depth_list, depth) 183 | 184 | depth_vars = Enum.map(depth_list, &elem(&1, 0)) 185 | 186 | case {depth_list, variable.type} do 187 | {[], :single} -> 188 | {[variable | variables], Map.put(variable_map, variable.name, variable), []} 189 | 190 | {[], :map} -> 191 | {[%Variable{variable | value: %{}} | variables], Map.put(variable_map, variable.name, variable), 192 | [{variable.name, depth}]} 193 | 194 | {[_], :single} -> 195 | parent = hd(variables) 196 | 197 | parent = %Variable{ 198 | parent 199 | | value: Map.put(parent.value, variable.name, variable.value), 200 | children: parent.children ++ [depth_vars ++ [variable.name]] 201 | } 202 | 203 | child = %Variable{variable | value: depth_vars, type: :map_child} 204 | {[parent | tl(variables)], Map.put(variable_map, child.name, child), depth_list} 205 | 206 | {[_], :map} -> 207 | parent = hd(variables) 208 | 209 | parent = %Variable{ 210 | parent 211 | | value: Map.put(parent.value, variable.name, variable.value) 212 | } 213 | 214 | child = %Variable{variable | value: depth_vars, type: :map_child_map} 215 | 216 | {[parent | tl(variables)], Map.put(variable_map, child.name, child), depth_list ++ [{variable.name, depth}]} 217 | 218 | {[_ | path], type} -> 219 | child_type = 220 | if type == :single do 221 | :map_child 222 | else 223 | :map_child_map 224 | end 225 | 226 | child = %Variable{variable | value: depth_vars, type: child_type} 227 | 228 | parent = hd(variables) 229 | 230 | children = 231 | case type do 232 | :map -> parent.children 233 | :single -> parent.children ++ [depth_vars ++ [variable.name]] 234 | end 235 | 236 | parent = %Variable{ 237 | parent 238 | | value: put_in(parent.value, Enum.map(path, &elem(&1, 0)) ++ [variable.name], variable.value), 239 | children: children 240 | } 241 | 242 | {[parent | tl(variables)], Map.put(variable_map, child.name, child), depth_list} 243 | 244 | {_, _} -> 245 | {variables, variable_map, depth_list} 246 | end 247 | end) 248 | 249 | variable_map = 250 | Enum.reduce(variables, variable_map, fn %{name: name} = var, variable_map -> 251 | Map.put(variable_map, name, var) 252 | end) 253 | 254 | {Enum.reverse(variables), variable_map} 255 | end 256 | 257 | defp prune_depth_list([], _), do: [] 258 | 259 | defp prune_depth_list(list, depth) do 260 | case List.pop_at(list, -1) do 261 | {{_, d}, rest} when d >= depth -> prune_depth_list(rest, depth) 262 | _ -> list 263 | end 264 | end 265 | 266 | defp variable_line_to_variable([depth, name | rest] = line) do 267 | {pic, rest} = parse_var_field(rest, :pic) 268 | pic = parse_pic(pic) 269 | {value, rest} = parse_var_field(rest, :value) 270 | constant = :constant in rest 271 | 272 | if rest != [] do 273 | # coveralls-ignore-start 274 | Logger.warn("Variable contained unexpected values: #{inspect(rest)}. Full variable: #{inspect(line)}") 275 | 276 | # coveralls-ignore-end 277 | end 278 | 279 | value = 280 | case {value, pic} do 281 | {value, {:str, _, length}} when value in [nil, :spaces] -> 282 | String.duplicate(" ", length) 283 | 284 | {value, {:str, _, length}} -> 285 | value |> trim_quotes() |> String.slice(0..(length - 1)) |> String.pad_trailing(length) 286 | 287 | {value, {:int, _, _}} when value in [nil, :zeros] -> 288 | 0 289 | 290 | {value, {:int, pic_str, length}} -> 291 | if !is_nil(value) and String.length(value) > length do 292 | raise "Variable #{name} has value #{value} which is larger than pic (#{pic_str}) allows" 293 | end 294 | 295 | value 296 | 297 | {nil, nil} -> 298 | nil 299 | 300 | _ -> 301 | Logger.warn(label: "unexpected variable_line_to_variable: #{inspect(line)}") 302 | value 303 | end 304 | 305 | %Variable{ 306 | depth: depth, 307 | name: name, 308 | type: 309 | if is_nil(pic) and !constant and is_nil(value) do 310 | :map 311 | else 312 | :single 313 | end, 314 | pic: pic, 315 | value: value || constant || %{}, 316 | constant: constant 317 | } 318 | end 319 | 320 | defp trim_quotes(str) do 321 | cond do 322 | String.starts_with?(str, "\"") && String.ends_with?(str, "\"") -> 323 | String.slice(str, 1..(String.length(str) - 2)) 324 | 325 | String.starts_with?(str, "'") && String.ends_with?(str, "'") -> 326 | String.slice(str, 1..(String.length(str) - 2)) 327 | 328 | true -> 329 | raise "String variable #{str} did not start and end with quotes" 330 | end 331 | end 332 | 333 | defp parse_var_field(var, field) do 334 | var 335 | |> Enum.split_with(&match?({^field, _}, &1)) 336 | |> case do 337 | {[{^field, val}], rest} -> {val, rest} 338 | {[], rest} -> {nil, rest} 339 | end 340 | end 341 | 342 | defp parse_pic(nil), do: nil 343 | 344 | defp parse_pic(pic) do 345 | pic = 346 | case Regex.run(~r/^([9|X])\((\d+)\)/, pic) do 347 | [_, type, count] -> String.duplicate(type, String.to_integer(count)) 348 | nil -> pic 349 | end 350 | 351 | split = String.split(pic, "", trim: true) 352 | 353 | cond do 354 | Enum.all?(split, &(&1 == "X")) -> {:str, pic, String.length(pic)} 355 | Enum.all?(split, &(&1 == "9")) -> {:int, pic, String.length(pic)} 356 | true -> {:other, pic} 357 | end 358 | end 359 | end 360 | --------------------------------------------------------------------------------