iso8583 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/AUTHORS ADDED
@@ -0,0 +1 @@
1
+ Tim Becker (tim@kuriositaet.de)
File without changes
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2009 Tim Becker
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
13
+ all 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
21
+ THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,44 @@
1
+
2
+ = ISO 8583 Financial Messaging for Ruby
3
+
4
+ This package currently contains code for coding an decoding ISO 8583
5
+ Financial Message.
6
+
7
+
8
+ == Installing
9
+
10
+ You can install the +hello+ package by executing:
11
+
12
+ gem install iso7816 -r
13
+
14
+ alternatively, you can download +.tar.gz+ or +.zip+ archives from
15
+ Rubyforge[http://rubyforge.org/frs/?group_id=1791].
16
+
17
+ == Source
18
+
19
+ The source is most readily available on github[http://github.com/a2800276/8583/],
20
+ and there should usually be a more or less current version available
21
+ in the Rubyforge SVN repository.
22
+
23
+
24
+ == Mailing List
25
+
26
+ In case you discover bugs, spelling errors, offer suggestions for
27
+ improvements or would like to help out with the project, you can contact
28
+ me directly (tim@kuriositaet.de).
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+ =
@@ -0,0 +1,131 @@
1
+ require "rake/rdoctask"
2
+ require "rake/gempackagetask"
3
+ require "rake/testtask"
4
+ require "rake/clean"
5
+ require "rubygems"
6
+
7
+ # Some definitions that you'll need to edit in case you reuse this
8
+ # Rakefile for your own project.
9
+
10
+ SHORTNAME ='iso8583' # this should be the rubyforge project name
11
+ DESC ="Ruby implementation of ISO 8583 financial messaging"
12
+ PKG_VERSION ='0.1.0'
13
+ LONG_DESC = <<END_DESC
14
+ Ruby implementation of ISO 8583 financial messaging
15
+ END_DESC
16
+ RUBYFORGE_USER ='a2800276'
17
+
18
+ # Specifies the default task to execute. This is often the "test" task
19
+ # and we'll change things around as soon as we have some tests.
20
+
21
+ task :default => [:rdoc]
22
+
23
+ # The directory to generate +rdoc+ in.
24
+ RDOC_DIR="doc/html"
25
+
26
+ # This global variable contains files that will be erased by the `clean` task.
27
+ # The `clean` task itself is automatically generated by requiring `rake/clean`.
28
+
29
+ CLEAN << RDOC_DIR << "pkg"
30
+
31
+
32
+ # This is the task that generates the +rdoc+ documentation from the
33
+ # source files. Instantiating Rake::RDocTask automatically generates a
34
+ # task called `rdoc`.
35
+
36
+ Rake::RDocTask.new do |rd|
37
+ # Options for documenation generation are specified inside of
38
+ # this block. For example the following line specifies that the
39
+ # content of the README file should be the main page of the
40
+ # documenation.
41
+ rd.main = "README"
42
+
43
+ # The following line specifies all the files to extract
44
+ # documenation from.
45
+ rd.rdoc_files.include( "README", "AUTHORS", "LICENSE", "TODO",
46
+ "CHANGELOG", "bin/**/*", "lib/**/*.rb",
47
+ "examples/**/*rb", "doc/*.rdoc")
48
+ # This one specifies the output directory ...
49
+ rd.rdoc_dir = "doc/html"
50
+
51
+ # Or the HTML title of the generated documentation set.
52
+ rd.title = "#{SHORTNAME}: #{DESC}"
53
+
54
+ # These are options specifiying how source code inlined in the
55
+ # documentation should be formatted.
56
+
57
+ rd.options = ["--line-numbers", "--inline-source"]
58
+
59
+ # Check:
60
+ # `rdoc --help` for more rdoc options
61
+ # the {rdoc documenation home}[http://www.ruby-doc.org/stdlib/libdoc/rdoc/rdoc/index.html]
62
+ # or the documentation for the +Rake::RDocTask+ task[http://rake.rubyforge.org/classes/Rake/RDocTask.html]
63
+ end
64
+
65
+ # The GemPackageTask facilitates getting all your files collected
66
+ # together into gem archives. You can also use it to generate tarball
67
+ # and zip archives.
68
+
69
+ # First you'll need to assemble a gemspec
70
+
71
+ PKG_FILES = FileList['lib/**/*.rb', 'bin/**/*', 'examples/**/*', '[A-Z]*', 'test/**/*'].to_a
72
+
73
+ spec = Gem::Specification.new do |s|
74
+ s.platform = Gem::Platform::RUBY
75
+ s.summary = "#{SHORTNAME}: #{DESC}"
76
+ s.name = SHORTNAME
77
+ s.rubyforge_project = SHORTNAME
78
+ s.version = PKG_VERSION
79
+ s.files = PKG_FILES
80
+ s.requirements << "none"
81
+ s.require_path = 'lib'
82
+ s.description = LONG_DESC
83
+ s.has_rdoc=true
84
+ s.author="Tim Becker"
85
+ s.email="tim.becker@kuriositaet.de"
86
+ s.homepage="http://iso8583.rubyforge.org/"
87
+
88
+ end
89
+
90
+ # Adding a new GemPackageTask adds a task named `package`, which generates
91
+ # packages as gems, tarball and zip archives.
92
+ Rake::GemPackageTask.new(spec) do |pkg|
93
+ pkg.need_zip = true
94
+ pkg.need_tar_gz = true
95
+ end
96
+
97
+
98
+ # This task is used to demonstrate how to upload files to Rubyforge.
99
+ # Calling `upload_page` creates a current version of the +rdoc+
100
+ # documentation and uploads it to the Rubyforge homepage of the project,
101
+ # assuming it's hosted there and naming conventions haven't changed.
102
+ #
103
+ # This task uses `sh` to call the `scp` binary, which is plattform
104
+ # dependant and may not be installed on your computer if you're using
105
+ # Windows. I'm currently not aware of any pure ruby way to do scp
106
+ # transfers.
107
+
108
+ RubyForgeProject=SHORTNAME
109
+
110
+ desc "Upload the web pages to the web."
111
+ task :upload_pages => ["rdoc"] do
112
+ if RubyForgeProject then
113
+ path = "/var/www/gforge-projects/#{RubyForgeProject}"
114
+ sh "scp -r doc/html/* #{RUBYFORGE_USER}@rubyforge.org:#{path}"
115
+ sh "scp doc/images/*.png #{RUBYFORGE_USER}@rubyforge.org:#{path}/images"
116
+ end
117
+ end
118
+
119
+ # This task will run the unit tests provided in files called
120
+ # `test/test*.rb`. The task itself can be run with a call to `rake test`
121
+
122
+ Rake::TestTask.new do |t|
123
+ t.libs << "test"
124
+ t.libs << "lib"
125
+ t.ruby_opts=["-rubygems"]
126
+ t.test_files = FileList['test/*.rb']
127
+ t.verbose = true
128
+ end
129
+
130
+
131
+
data/TODO ADDED
@@ -0,0 +1,10 @@
1
+
2
+ * mti_required : or something of the sort to indicate which fields are
3
+ required/mirrored/conditional for each message type
4
+
5
+ * decode_check : currently the codecs are very liberal about what to accept
6
+ or parse. E.g. if an invalid date is received, it doesn't complain. Consider
7
+ collecting the errors and presenting them through in interface so
8
+ it may be decided after parsing the message whether to reject it or no
9
+
10
+ * general cleanup/license/docs/readme
@@ -0,0 +1,79 @@
1
+ # Copyright 2009 by Tim Becker (tim.becker@kuriostaet.de)
2
+ # MIT License, for details, see the LICENSE file accompaning
3
+ # this distribution
4
+
5
+ require 'lib/iso8583'
6
+
7
+
8
+ # Example of a protocol specification based on:
9
+ # http://www.berlin-group.org/documents/BG_Authorisation_3.0.pdf
10
+ # The Berlin Groups Authorisation Interface specification.
11
+ # No gurantees are made that this is an accurate implemenation.
12
+ # It currently serves as an example only.
13
+
14
+
15
+ class BerlinMessage < Message
16
+ mti_format N, :length=>4
17
+ mti 1100, "Authorization Request Acquirer Gateway"
18
+ mti 1110, "Authorization Request Response Issuer Gateway"
19
+ mti 1420, "Reversal Advice Acquirer Gateway"
20
+ mti 1421, "Reversal Advice Repeat Acquirer Gateway"
21
+ mti 1430, "Reversal Advice Response Issuer Gateway"
22
+ mti 1804, "Network Management Request Acquirer Gateway or Issuer Gateway"
23
+ mti 1814, "Network Management Request Response Issuer Gateway or Acquirer Gateway"
24
+
25
+ bmp 2, "Primary Account Number (PAN)", LLVAR_N, :max => 19
26
+ bmp 3, "Processing Code", N, :length => 6
27
+ bmp 4, "Amount (Transaction)", N, :length => 12
28
+ bmp 6, "Amount, Cardholder Billing" , N, :length => 12
29
+ bmp 7, "Date and Time, Transmission" , MMDDhhmmss
30
+ bmp 10, "Conversion Rate, Cardholder Billing", N, :length => 8
31
+ bmp 11, "System Trace Audit Number (STAN)", N, :length => 6
32
+ bmp 12, "Date and Time, Local Transaction", YYMMDDhhmmss
33
+ bmp 14, "Date, Expiration", YYMM
34
+ bmp 22, "POS Data Code", AN, :length => 12
35
+ bmp 23, "Card Sequence Number", N, :length => 3
36
+ bmp 24, "Function Code", N, :length => 3
37
+ bmp 25, "Message Reason Code", N, :length => 4
38
+ bmp 26, "Card Acceptor Business Code", N, :length => 4
39
+ bmp 30, "Amounts, Original", N, :length => 24
40
+ bmp 32, "Acquiring Institution Identification Code", LLVAR_N, :max => 11
41
+ bmp 35, "Track 2 Data", LLVAR_Z, :max => 37
42
+ bmp 37, "Retrieval Reference Number", ANP, :length => 12
43
+ bmp 38, "Approval Code", ANP, :length => 6
44
+ bmp 39, "Action Code", N, :length => 3
45
+ bmp 41, "Card Acceptor Terminal Identification", ANS, :length => 8
46
+ bmp 42, "Card Acceptor Identification Code", ANS, :length => 15
47
+ bmp 43, "Card Acceptor Name/Location", LLVAR_ANS, :max => 56
48
+ bmp 49, "Currency Code, Transaction", N, :length => 3
49
+ bmp 51, "Currency Code, Cardholder Billing", N, :length => 3
50
+ bmp 52, "Personal Identification Number (PIN) Data", B, :length => 8
51
+ bmp 53, "Security Related Control Information", LLVAR_B, :max => 48
52
+ bmp 54, "Amounts, Additional", LLLVAR_ANS,:max => 40
53
+
54
+ bmp 55, "Integrated Circuit Card (ICC) System Related Data", LLLVAR_B, :max => 255
55
+ bmp 56, "Original Data Elements", LLVAR_N, :max => 35
56
+ bmp 58, "Authorizing Agent Institution Identification Code", LLVAR_N, :max => 11
57
+ bmp 59, "Additional Data - Private", LLLVAR_ANS, :max => 67
58
+ bmp 64, "Message Authentication Code (MAC) Field", B, :length => 8
59
+
60
+ bmp_alias 2, :pan
61
+ bmp_alias 3, :proc_code
62
+ bmp_alias 4, :amount
63
+ bmp_alias 12, :exp_date
64
+ end
65
+
66
+ if __FILE__==$0
67
+ mes = BerlinMessage.new
68
+ mes.mti = 1110
69
+ mes[2] = 474747474747
70
+ mes["Processing Code"] = "123456"
71
+
72
+ pan = mes["Primary Account Number (PAN)"]
73
+ #mes.pan = 47474747474747
74
+
75
+ #puts mes.pan
76
+ puts mes.to_b
77
+ puts mes.to_s
78
+ #mes2 = BerlinMessage.parse input
79
+ end
@@ -0,0 +1,117 @@
1
+ # Copyright 2009 by Tim Becker (tim.becker@kuriostaet.de)
2
+ # MIT License, for details, see the LICENSE file accompaning
3
+ # this distribution
4
+
5
+ # This class constructs an object for handling bitmaps
6
+ # with which ISO8583 messages typically begin.
7
+ # Bitmaps are either 8 or 16 bytes long, an extended length
8
+ # bitmap is indicated by the first bit being set.
9
+ # In all likelyhood, you won't be using this class much, it's used
10
+ # transparently by the Message class.
11
+ class Bitmap
12
+
13
+
14
+ # create a new Bitmap object. In case an iso message
15
+ # is passed in, that messages bitmap will be parsed. If
16
+ # not, this initializes and empty bitmap.
17
+
18
+ def initialize message=nil
19
+ @bmp = Array.new(128,false)
20
+ if !message
21
+
22
+ else
23
+ initialize_from_message message
24
+ end
25
+ end
26
+
27
+ # yield once with the number of each set field.
28
+ def each #:yields: each bit set in the bitmap.
29
+ @bmp.each_with_index {|set,i| yield i+i if set}
30
+ end
31
+
32
+ # Returns whether the bit is set or not.
33
+ def [] i
34
+ @bmp[i-1]
35
+ end
36
+
37
+ # Set the bit to the indicated value. Only `true` sets the
38
+ # bit, any other value unsets it.
39
+ def []= i,value
40
+ if i > 128
41
+ raise ISO8583Exception.new("Bits > 128 are not permitted.")
42
+ elsif i < 2
43
+ raise ISO8583Exception.new("Bits < 2 are not permitted (continutation bit is set automatically)")
44
+ end
45
+ @bmp[i-1]=(value==true)
46
+ end
47
+
48
+ # Sets bit #i
49
+ def set i
50
+ self[i]=true
51
+ end
52
+
53
+ # Unsets bit #i
54
+ def unset i
55
+ self[i]=false
56
+ end
57
+
58
+ # Generate the bytes representing this bitmap.
59
+ def to_bytes
60
+ arr=[self.to_s]
61
+ # tricky and ugly, setting bit[1] only when generating to_s...
62
+ count = self[1] ? 128 : 64
63
+ arr.pack("B#{count}")
64
+ end
65
+ alias_method :to_b :to_bytes
66
+
67
+ # Generate a String representation of this bitmap in the form:
68
+ # 01001100110000011010110110010100100110011000001101011011001010
69
+ def to_s
70
+ #check whether any `high` bits are set
71
+ @bmp[0]=false
72
+ 65.upto(128) { |i|
73
+ if self[i]
74
+ # if so, set continuation bit
75
+ @bmp[0]=true
76
+ break
77
+ end
78
+ }
79
+ str = ""
80
+ 1.upto(self[1] ? 128 : 64) {|i|
81
+ str << (self[i] ? "1" : "0")
82
+ }
83
+ str
84
+ end
85
+
86
+ private
87
+ def initialize_from_message message
88
+ bmp = message.unpack("B64")[0]
89
+ if bmp[0,1] == "1"
90
+ bmp = message.unpack("B128")[0]
91
+ end
92
+
93
+ 0.upto(bmp.length-1) do |i|
94
+ @bmp[i] = bmp[i,1]=="1"
95
+ end
96
+
97
+
98
+ end
99
+
100
+ class << self
101
+ # Parse the bytes in string and return the Bitmap and bytes remaining in `str`
102
+ # after the bitmap is taken away.
103
+ def parse str
104
+ bmp = Bitmap.new(str)
105
+ rest = bmp[1] ? str[16,str.length] : str[8,str.length]
106
+ return [bmp,rest]
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ if __FILE__==$0
113
+
114
+ mp = ISO8583::Bitmap.new
115
+ 20.step(128,7) {|i| mp.set(i)}
116
+ print mp.to_bytes
117
+ end
@@ -0,0 +1,173 @@
1
+ # Copyright 2009 by Tim Becker (tim.becker@kuriostaet.de)
2
+ # MIT License, for details, see the LICENSE file accompaning
3
+ # this distribution
4
+
5
+ require 'date'
6
+
7
+ # Codec provides functionality to encode and decode values, codecs are
8
+ # used internally by Field instances in order to do character conversions
9
+ # and checking for proper values.
10
+ # Although they are used internally, you will probably need to write
11
+ # your own Codec sooner or later. The codecs used by Field instances are
12
+ # typically instances of Codec, it may or may not be usefull to subclass
13
+ # Codec.
14
+ #
15
+ # Say, for example, a text field needs to be encoded in EBCDIC in the
16
+ # message, this is how a corresponding codec would be constructed:
17
+ #
18
+ # EBCDIC_Codec = Codec.new
19
+ # EBCDIC_Codec.encoder = lambda {|ascii_str|
20
+ # raise ISO8583Exception.new("String (#{ascii_str})not valid!") unless =~ /someregexp/
21
+ # ascii2ebcdic ascii_str # implementing ascii_str is left as an excercise
22
+ # }
23
+ # EBCDIC_Codec.decode = lambda {|ebcdic_str|
24
+ # # you may or may not want to raise exceptions at this point ....
25
+ # # strip removes any padding...
26
+ # ebcdic2ascii(ebcdic_str).strip
27
+ # }
28
+ #
29
+ # This instance of Codec would then be used be the corresponding Field
30
+ # encoder/decoder, which may look similar to this:
31
+ #
32
+ # EBCDIC = Field.new
33
+ # EBCDIC.codec = EBCDIC_Codec
34
+ # EBCDIC.padding = PADDING_LEFT_JUSTIFIED_SPACES
35
+ #
36
+ # Notice there is a bit of inconsistancy: the padding is added by the
37
+ # field, but removed by the codec. I would like to find a better
38
+ # solution to this...
39
+ #
40
+ # See also: Field, link:files/lib/fields_rb.html
41
+ #
42
+ # The following codecs are already implemented:
43
+ # [+ASCII_Number+] encodes either a Number or String representation of
44
+ # a number to the ASCII represenation of the number,
45
+ # decodes ASCII numerals to a number
46
+ # [+AN_Codec+] passes through ASCII string checking they conform to [A-Za-z0-9]
47
+ # during encoding, no validity check during decoding.
48
+ # [+ANP_Codec+] passes through ASCII string checking they conform to [A-Za-z0-9 ]
49
+ # during encoding, no validity check during decoding.
50
+ # [+ANS_Codec+] passes through ASCII string checking they conform to [\x20-\x7E]
51
+ # during encoding, no validity check during decoding.
52
+ # [+Null_Codec+] passes anything along untouched.
53
+ # [<tt>Track2</tt>] rudimentary check that string conforms to Track2
54
+ # [+MMDDhhmmssCodec+] encodes Time, Datetime or String to the described date format, checking
55
+ # that it is a valid date. Decodes to a DateTime instance, decoding and
56
+ # encoding perform validity checks!
57
+ # [+YYMMDDhhmmssCodec+] encodes Time, Datetime or String to the described date format, checking
58
+ # that it is a valid date. Decodes to a DateTime instance, decoding and
59
+ # encoding perform validity checks!
60
+ # [+YYMMCodec+] encodes Time, Datetime or String to the described date format (exp date),
61
+ # checking that it is a valid date. Decodes to a DateTime instance, decoding
62
+ # and encoding perform validity checks!
63
+ #
64
+ class Codec
65
+ attr_accessor :encoder
66
+ attr_accessor :decoder
67
+
68
+ def decode raw
69
+ return decoder.call(raw)
70
+ end
71
+
72
+ # length is either a fixnum or a lenth encoder.
73
+ def encode value
74
+ return encoder.call(value)
75
+ end
76
+ end
77
+
78
+ # ASCII_Number
79
+ ASCII_Number = Codec.new
80
+ ASCII_Number.encoder= lambda{|num|
81
+ enc = num.to_s
82
+ raise ISO8583Exception.new("Invalid value: #{enc} must be numeric!") unless enc =~ /^[0-9]*$/
83
+ enc
84
+ }
85
+
86
+ ASCII_Number.decoder= lambda{|raw|
87
+ raw.to_i
88
+ }
89
+
90
+ PASS_THROUGH_DECODER = lambda{|str|
91
+ str.strip # remove padding
92
+ }
93
+
94
+ AN_Codec = Codec.new
95
+ AN_Codec.encoder = lambda{|str|
96
+ raise ISO8583Exception.new("Invalid value: #{str} must be [A-Za-y0-9]") unless str =~ /^[A-Za-z0-9]*$/
97
+ str
98
+ }
99
+ AN_Codec.decoder = PASS_THROUGH_DECODER
100
+
101
+ ANP_Codec = Codec.new
102
+ ANP_Codec.encoder = lambda{|str|
103
+ raise ISO8583Exception.new("Invalid value: #{str} must be [A-Za-y0-9 ]") unless str =~ /^[A-Za-z0-9 ]*$/
104
+ str
105
+ }
106
+ ANP_Codec.decoder = PASS_THROUGH_DECODER
107
+
108
+ ANS_Codec = Codec.new
109
+ ANS_Codec.encoder = lambda{|str|
110
+ raise ISO8583Exception.new("Invalid value: #{str} must be [\x20-\x7E]") unless str =~ /^[\x20-\x7E]*$/
111
+ str
112
+ }
113
+ ANS_Codec.decoder = PASS_THROUGH_DECODER
114
+
115
+ Null_Codec = Codec.new
116
+ Null_Codec.encoder = lambda {|str|
117
+ str
118
+ }
119
+ Null_Codec.decoder = lambda {|str|
120
+ str.gsub(/\000*$/,'')
121
+ }
122
+
123
+ Track2 = Codec.new
124
+ Track2.encoder = lambda{|track2|
125
+ #SS | PAN | FS | Expiration Date | Service Code | Discretionary Data | ES | LRC
126
+ # SS = ;
127
+ # PAN = up to 19 digits (at least 9?)
128
+ # FS = '='
129
+ # Exp Date = YYMM
130
+ # SC: 3 digits or =
131
+ # ES = ?
132
+ # lrc : 1byte
133
+ raise ISO8583Exception.new("Invalid Track2 data: #{track2}") unless track2 =~ /^;*(\d{9,19})=(.*)\?.$/
134
+ track2
135
+ }
136
+ Track2.decoder = PASS_THROUGH_DECODER
137
+
138
+ def _date_codec(fmt)
139
+ c = Codec.new
140
+ c.encoder = lambda {|date|
141
+ enc = case date
142
+ when Time
143
+ when DateTime
144
+ date.strftime(fmt)
145
+ when String
146
+ begin
147
+ dt = DateTime.strptime date, fmt
148
+ dt.strftime(fmt)
149
+ rescue
150
+ raise ISO8583Exception.new("Invalid format encoding: #{date}, must be #{fmt}.")
151
+ end
152
+ else
153
+ raise ISO8583Exception.new("Don't know how to encode: #{date.class} to a time.")
154
+ end
155
+ return enc
156
+ }
157
+ c.decoder = lambda {|str|
158
+ dt = begin
159
+ DateTime.strptime str, fmt
160
+ rescue
161
+ raise ISO8583Exception.new("Invalid format decoding: #{str}, must be MMDDhhmmss.")
162
+ end
163
+ return dt
164
+ }
165
+
166
+ return c
167
+ end
168
+
169
+ MMDDhhmmssCodec = _date_codec("%m%d%H%M%S")
170
+ YYMMDDhhmmssCodec = _date_codec("%y%m%d%H%M%S")
171
+ YYMMCodec = _date_codec("%y%m")
172
+
173
+