fbtxt-pp 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cb82751c433e1b2a9495f545dde77a538f1cb08b66e55aa6c6af0e3830ef6cff
4
+ data.tar.gz: 5cc0c7f57f4d5e79db8bba38d97dc3dbc4ca6374de7db8d51acf3fc7d351e681
5
+ SHA512:
6
+ metadata.gz: ec8a5d094a5054b95a7ee2fd7b2d798a181c4ad92e81f77a1d94bf571041032431d69fa85afbf90f07bd1bd3e35306a8d45a8a994a0989c6760e8321231d99d2
7
+ data.tar.gz: 32dea04aa3ad82a0033c8505794e2dd6fb959f0fdef51a00dbacd53a083358f0a2ac81628f1feccb745169e163f6c1301400de2c26487167aa2b0064f734551d
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+
2
+ ### 0.0.2 / 2026-07-23
3
+
4
+ * Everything is new. First release.
data/Manifest.txt ADDED
@@ -0,0 +1,34 @@
1
+ CHANGELOG.md
2
+ Manifest.txt
3
+ README.md
4
+ Rakefile
5
+ bin/fbhub
6
+ bin/fbpp
7
+ lib/fbtxt-pp.rb
8
+ lib/fbtxt-pp/config.rb
9
+ lib/fbtxt-pp/config_clubs.csv
10
+ lib/fbtxt-pp/config_more.rb
11
+ lib/fbtxt-pp/config_worldcup.rb
12
+ lib/fbtxt-pp/helper.rb
13
+ lib/fbtxt-pp/models/document.rb
14
+ lib/fbtxt-pp/models/goals.rb
15
+ lib/fbtxt-pp/models/match.rb
16
+ lib/fbtxt-pp/models/officials.rb
17
+ lib/fbtxt-pp/models/penalties.rb
18
+ lib/fbtxt-pp/models/players.rb
19
+ lib/fbtxt-pp/models/score.rb
20
+ lib/fbtxt-pp/models/stadiums.rb
21
+ lib/fbtxt-pp/models/teams.rb
22
+ lib/fbtxt-pp/pp/ppbookings.rb
23
+ lib/fbtxt-pp/pp/ppgoals.rb
24
+ lib/fbtxt-pp/pp/pplineup.rb
25
+ lib/fbtxt-pp/pp/ppmatch.rb
26
+ lib/fbtxt-pp/pp/ppmatch_full.rb
27
+ lib/fbtxt-pp/pp/ppmatch_min.rb
28
+ lib/fbtxt-pp/pp/ppopts.rb
29
+ lib/fbtxt-pp/pp/pppenalties.rb
30
+ lib/fbtxt-pp/pp/ppsquads.rb
31
+ lib/fbtxt-pp/pp/ppstats.rb
32
+ lib/fbtxt-pp/tool-fbhub.rb
33
+ lib/fbtxt-pp/tool-fbpp.rb
34
+ lib/fbtxt-pp/version.rb
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # fbtxt-pp - pretty print (pp) football match fixtures & results in the football.txt format
2
+
3
+
4
+ * home :: [github.com/sportdb/sport.db](https://github.com/sportdb/sport.db)
5
+ * bugs :: [github.com/sportdb/sport.db/issues](https://github.com/sportdb/sport.db/issues)
6
+ * gem :: [rubygems.org/gems/fbtxt-pp](https://rubygems.org/gems/fbtxt-pp)
7
+ * rdoc :: [rubydoc.info/gems/fbtxt-pp](http://rubydoc.info/gems/fbtxt-pp)
8
+
9
+
10
+
11
+ ## Usage
12
+
13
+ To be done
14
+
15
+
16
+ ## License
17
+
18
+ The `fbtxt-pp` scripts are dedicated to the public domain.
19
+ Use as you please with no restrictions whatsoever.
20
+
21
+
22
+ ## Questions? Comments?
23
+
24
+ Yes, you can. More than welcome.
25
+ See [Help & Support »](https://github.com/openfootball/help)
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ require 'hoe'
2
+ require './lib/fbtxt-pp/version.rb'
3
+
4
+
5
+ Hoe.spec 'fbtxt-pp' do
6
+
7
+ self.version = Fbtxt::Module::Fbpp::VERSION
8
+
9
+ self.summary = "fbtxt-pp - pretty print (pp) football match fixtures & results in the football.txt format"
10
+ self.description = summary
11
+
12
+ self.urls = { home: 'https://github.com/sportdb/sport.db' }
13
+
14
+ self.author = 'Gerald Bauer'
15
+ self.email = 'gerald.bauer@gmail.com'
16
+
17
+ # switch extension to .markdown for gihub formatting
18
+ self.readme_file = 'README.md'
19
+ self.history_file = 'CHANGELOG.md'
20
+
21
+ self.extra_deps = [
22
+ ['cocos'],
23
+ ['season-formats'],
24
+ ]
25
+
26
+ self.licenses = ['Public Domain']
27
+
28
+ self.spec_extras = {
29
+ required_ruby_version: '>= 3.1.0'
30
+ }
31
+
32
+ end
data/bin/fbhub ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ## tip: to test run:
4
+ ## ruby -I ./lib bin/fbpp
5
+
6
+
7
+ ##
8
+ ## note - always use latest (local) if present
9
+ $LOAD_PATH.unshift( '/sports/yorobot/fbup/fbup/lib' )
10
+ require 'fbup'
11
+
12
+ ##
13
+ ## note - always use latest (local) if present
14
+ $LOAD_PATH.unshift( '/sports/yorobot/sport.db.more/fbtxt-pp/lib' )
15
+ require 'fbtxt-pp'
16
+
17
+
18
+ Fbhub.main( ARGV )
data/bin/fbpp ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ## tip: to test run:
4
+ ## ruby -I ./lib bin/fbpp
5
+
6
+
7
+ ##
8
+ ## note - always use latest (local) if present
9
+ $LOAD_PATH.unshift( '/sports/yorobot/sport.db.more/fbtxt-pp/lib' )
10
+
11
+ require 'fbtxt-pp'
12
+
13
+
14
+ Fbpp.main( ARGV )
@@ -0,0 +1,62 @@
1
+
2
+
3
+
4
+ ## defaults
5
+ FORMAT_OPTS_DEFAULTS = {
6
+ country: false,
7
+ city: false,
8
+ stadium: false,
9
+ timezone: false,
10
+ }
11
+
12
+ FORMAT_OPTS_FULL_DEFAULTS = {
13
+ country: false,
14
+ city: true,
15
+ stadium: true,
16
+ timezone: true,
17
+
18
+ show_teams: false,
19
+ show_stadiums: false,
20
+ }
21
+
22
+ def _parse_format_opts( str )
23
+ h = {}
24
+ keys = str.split( /[ ]*\|[ ]*/ )
25
+ keys.each do |key|
26
+ case key.to_sym
27
+ when :country then h[:country] = true
28
+ when :city then h[:city] = true
29
+ else
30
+ raise ArgumentError, "unknown key #{key} in format opts"
31
+ end
32
+ end
33
+ h
34
+ end
35
+
36
+
37
+ def read_config_pp( *paths )
38
+ config = {}
39
+ paths.each do |path|
40
+ recs = read_csv( path )
41
+
42
+ recs.each do |rec|
43
+ key = rec['code']
44
+ h = {
45
+ slug: key,
46
+ name: rec['name'],
47
+ seasons: rec['seasons'],
48
+ opts: {}.merge( FORMAT_OPTS_DEFAULTS, _parse_format_opts( rec['opts'] )),
49
+ opts_full: {}.merge( FORMAT_OPTS_FULL_DEFAULTS, _parse_format_opts( rec['opts_full'] )),
50
+ }
51
+
52
+ config[ key ] = h
53
+ end
54
+ end
55
+ config
56
+ end
57
+
58
+
59
+ CONFIGS = {}
60
+ more_configs = read_config_pp( "#{Fbtxt::Module::Fbpp.root}/lib/fbtxt-pp/config_clubs.csv" )
61
+ pp more_configs
62
+ CONFIGS.merge!( more_configs )
@@ -0,0 +1,37 @@
1
+
2
+ code, name, seasons, opts, opts_full
3
+
4
+ ##
5
+ ## remove seasons - why? why not?
6
+
7
+ at, Austria | Bundesliga, 2025/26,,
8
+ de, Germany | Bundesliga, 2025/26,,
9
+ eng, England | Premier League, 2025/26,,
10
+
11
+
12
+ fr, France | Ligue 1, 2025/26,,
13
+ it, Italy | Serie A, 2025/26,,
14
+ es, Spain | Primera División, 2025/26,,
15
+
16
+ mx, Mexico | Liga MX, 2025/26,,
17
+
18
+ uefa.cl, UEFA | Champions League, 2025/26, country|city, country
19
+ uefa.europa, UEFA | Europa League, 2025/26, country|city, country
20
+ uefa.con, UEFA | Conference League, 2025/26, country|city, country
21
+
22
+ ###
23
+ # opts
24
+ # default as false everyting!! e.g.
25
+ # country: false,
26
+ # stadium: false,
27
+ # city: false,
28
+ # timezone: false
29
+ # { country: false,
30
+ # stadium: false,
31
+ # city: false,
32
+ # timezone: false,
33
+ # }
34
+ # opts_full
35
+ # default only country false
36
+ # { country: false,
37
+ # }
@@ -0,0 +1,65 @@
1
+
2
+ ### rename to setups or such - why? why not?
3
+
4
+
5
+ ## default setups
6
+
7
+ CONFIGS.merge!({
8
+ clubworldcup: {
9
+ slug: 'clubworldcup', ## rename to dir / indir / source / source or ???
10
+ name: 'Club World Cup',
11
+ seasons: ['2025'],
12
+ ## default pp opts
13
+ opts: { country: true,
14
+ show_stadiums: false,
15
+ },
16
+ opts_full: { country: true,
17
+ },
18
+
19
+ ## outdir = "../../openfootball/clubworldcup"
20
+ },
21
+
22
+
23
+
24
+ ## todo - find a better key than _v0??
25
+ ## use clubworldcup_hist or _history or __ ??
26
+ clubworldcup_v0: {
27
+ ## all club world cups 2000, 2005-2023
28
+ ## NOT incl. new format every 4 yrs starting in 2025
29
+ ## NOT incl. old/new interconti cup every yr starting in 2024
30
+ slug: 'interconticup',
31
+ name: 'Club World Cup',
32
+ ## outname: 'clubworldcup',
33
+ seasons: ['2000',
34
+ '2005', '2006', '2007', '2008', '2009',
35
+ '2010', '2011', '2012', '2013', '2014',
36
+ '2015', '2016', '2017', '2018', '2019',
37
+ '2020', '2021', '2022', '2023'],
38
+ ## default pp opts
39
+ opts: { country: true,
40
+ show_stadiums: false,
41
+ show_teams: true,
42
+ },
43
+ opts_full: { country: true,
44
+ show_teams: true,
45
+ },
46
+ ## outdir = "../../openfootball/clubworldcup"
47
+ },
48
+
49
+
50
+
51
+ interconticup: {
52
+ ## (new) interconti cup 2024-
53
+ slug: 'interconticup',
54
+ name: 'Intercontinental Cup',
55
+ seasons: ['2024', '2025'],
56
+ ## default pp opts
57
+ opts: { country: true,
58
+ show_stadiums: false,
59
+ show_teams: true,
60
+ },
61
+ opts_full: { country: true,
62
+ show_teams: true,
63
+ },
64
+ },
65
+ })
@@ -0,0 +1,27 @@
1
+
2
+ CONFIGS.merge!({
3
+ worldcup: {
4
+ slug: 'worldcup', ## rename to dir / indir / source / source or ???
5
+ name: 'World Cup',
6
+ ## check - incl. 2026 - not all matches with results and teams!!!
7
+ seasons: ['1930', '1934', '1938',
8
+ '1950', '1954', '1958', '1962', '1966', '1970', '1974', '1978',
9
+ '1982', '1986', '1990', '1994', '1998', '2002', '2006', '2010',
10
+ '2014', '2018', '2022', '2026'],
11
+ ## default pp opts
12
+ opts: { country: false,
13
+ stadium: false,
14
+ city: true,
15
+ show_stadiums: true,
16
+ show_teams: false,
17
+ timezone: true,
18
+ },
19
+ opts_full: { country: false,
20
+ show_stadiums: true,
21
+ },
22
+ ## opts for squads (incl. jersey numbers - yes/no?)
23
+ jerseys: ->(season) { season >= 1954 ? true : false },
24
+
25
+ ## outdir = "../../openfootball/worldcup"
26
+ }
27
+ })
@@ -0,0 +1,69 @@
1
+
2
+
3
+ def assert( test, msg )
4
+ if test
5
+ else
6
+ puts "!! ASSERT FAILED - #{msg}"
7
+ exit 1
8
+ end
9
+ end
10
+
11
+
12
+ def parse_date_utc( date_str )
13
+ ## note - DateTime has NOT daylight saving time (e.g. dst?)
14
+ ## or named timezones!!
15
+ ## only works with offsets
16
+ ##
17
+ ## use Time for built-in timezones (and check on daylight saving time etc.)
18
+
19
+ date = DateTime.strptime( date_str, '%Y-%m-%dT%H:%M%z' )
20
+
21
+ assert( date_str == date.strftime('%Y-%m-%dT%H:%MZ'),
22
+ "date parse expected #{date_str} - got #{date.inspect}" )
23
+ date
24
+ end
25
+
26
+ def parse_date_local( date_str )
27
+ ## fix - parse UTC+-offset !!!!
28
+ ## e.g. 2025-08-01 20:30 UTC+2
29
+ date = DateTime.strptime( date_str, '%Y-%m-%d %H:%M UTC%z' )
30
+
31
+ ## assert( date_str == date.strftime('%Y-%m-%dT%H:%MZ'),
32
+ ## "date parse expected #{date_str} - got #{date.inspect}" )
33
+ date
34
+ end
35
+
36
+
37
+
38
+ MINUTE_RE = %r{ \A
39
+ (?<minute>\d{1,3}) '?
40
+ ( \+
41
+ (?<offset>\d{1,2}) '?
42
+ )?
43
+ \z
44
+ }x
45
+
46
+
47
+ def _parse_minute( str )
48
+
49
+ ## support weirdo 120'+-30' -- remove minuts
50
+ str = str.gsub( '-', '' )
51
+
52
+ m = MINUTE_RE.match( str )
53
+ raise ArgumentError, "unknown goal minute format in #{str.inspect}" if m.nil?
54
+
55
+ minute = m[:minute].to_i(10)
56
+ offset = m[:offset] ? m[:offset].to_i(10) : nil
57
+
58
+ [minute,offset]
59
+ end
60
+
61
+ def _fmt_minute( minute, offset )
62
+ ## pp [minute,offset]
63
+
64
+ buf = String.new
65
+ buf << "#{minute}"
66
+ buf << "+#{offset}" if offset
67
+ buf << "'"
68
+ buf
69
+ end
@@ -0,0 +1,130 @@
1
+
2
+ ##
3
+ ## note:
4
+ ## document is container for LeagueSeason holding teams, matches, etc.
5
+
6
+ class Document
7
+
8
+ def self.read( path )
9
+ data = read_json( path )
10
+ new( data )
11
+ end
12
+
13
+ def initialize( data )
14
+ @data = data
15
+
16
+ @teams = Teams.new
17
+ @teams.add( data['teams'] )
18
+ ## puts " #{teams.size} team(s) in season #{season}"
19
+
20
+ @stadiums = Stadiums.new
21
+ @stadiums.add( data['stadiums'] )
22
+ ## puts " #{stadiums.size} stadium(s) in season #{season}"
23
+
24
+
25
+ ## read in stages for sorting
26
+ ## incl. SequenceOrder, StageLevel (optional)
27
+ ## stages = Stages.new
28
+ ## stages.add( read_json( "./#{slug}/misc/#{season}_stages.json" )['Results'] )
29
+
30
+ ## automagically sort (by groups) - why? why not?
31
+ @data['matches'] = _sort_matches( @data['matches'] )
32
+ end
33
+
34
+
35
+ def find_team!( q ) @teams.find!( q ); end
36
+
37
+ def find_stadium!( h ) @stadiums.find!( h ); end
38
+
39
+
40
+ ## quick ("unsafe/low-level") access for size - keep? why? why not?
41
+ ## use team_count, stadium_count, match_count - why? why not?
42
+ def teams() @teams; end
43
+ def stadiums() @stadiums; end
44
+ def matches() @data['matches']; end
45
+
46
+
47
+ def each_match( &blk )
48
+ @data['matches'].each do |_m|
49
+ ## use ("typed" struct) wrapper
50
+ m = Match.new( self, _m )
51
+ blk.call( m )
52
+ end
53
+ end
54
+ alias_method :each, :each_match
55
+
56
+
57
+ ## (was: collect_dates)
58
+ ## find a better name e.g. min_max_dates / duration / collect/find_start_end_dates?
59
+ def calc_start_end_dates
60
+ start_date = nil
61
+ end_date = nil
62
+
63
+ @data['matches'].each do |_m|
64
+
65
+ ## date_utc = parse_date_utc( _m['datetime_utc'] )
66
+ date_local = parse_date_local( _m['datetime_local'] )
67
+
68
+ ## note - alway use local datetime for now
69
+
70
+ if start_date.nil? || date_local < start_date
71
+ start_date = date_local
72
+ end
73
+
74
+ if end_date.nil? || date_local > end_date
75
+ end_date = date_local
76
+ end
77
+ end
78
+
79
+ [start_date, end_date]
80
+ end
81
+
82
+
83
+
84
+ ###
85
+ ## (private) helpers
86
+
87
+ def _sort_matches( matches )
88
+ ###
89
+ ## sort results by group if present
90
+
91
+ ## add "old" sort index
92
+ matches = matches.each_with_index.map {|m,i| m['sort']=i+1; m }
93
+
94
+
95
+ matches = matches.sort do |l,r|
96
+
97
+ lhs_stage = l['stage']
98
+ rhs_stage = r['stage']
99
+
100
+ ## lhs_stage = stages.find!( lhs_stageName )
101
+ ## rhs_stage = stages.find!( rhs_stageName )
102
+
103
+ lhs_group = l['group'] # optional
104
+ rhs_group = r['group'] # optional
105
+
106
+ lhs_matchday = l['matchday'] # optional
107
+ rhs_matchday = r['matchday'] # optional
108
+
109
+ if (lhs_group && rhs_group) && (lhs_stage == rhs_stage)
110
+ res = lhs_group <=> rhs_group
111
+ res = (lhs_matchday && rhs_matchday ?
112
+ lhs_matchday <=> rhs_matchday : 0 ) if res == 0
113
+ ## same group; sort by old index (or) date??
114
+ res = l['sort'] <=> r['sort'] if res == 0
115
+ res
116
+ else
117
+ ### sort first by stage (seq) and than keep as is
118
+ ### res = lhs_stage[:seq] <=> rhs_stage[:seq]
119
+ ## res = l['sort'] <=> r['sort'] if res == 0
120
+ ## res
121
+ l['sort'] <=> r['sort']
122
+ end
123
+ end
124
+
125
+ matches
126
+ end
127
+
128
+
129
+
130
+ end # class Document
@@ -0,0 +1,60 @@
1
+
2
+
3
+ class Goal
4
+ def self.build( h )
5
+ ## split into minute
6
+ ## and offset (stoppage/injury time)
7
+ ## e.g. 90'+11'
8
+
9
+ ##
10
+ ## keep minute as string and incl. stoppage/injury time - why? why not?
11
+
12
+ new( name: h['name'],
13
+ minute: h['minute'],
14
+ og: h['og'], # true|false
15
+ pen: h['pen'] # true|false
16
+ )
17
+ end
18
+
19
+
20
+ attr_reader :name,
21
+ :minute, :og, :pen
22
+
23
+ def initialize( name:, minute:,
24
+ og: false, pen: false )
25
+
26
+ norm = norm_name( name )
27
+ if norm != name
28
+ puts " NORM GOAL/PLAYER NAME >#{name}< => >#{norm}<"
29
+ name = norm
30
+ end
31
+
32
+ @name = name
33
+ @minute = minute
34
+
35
+ @og, @pen = og, pen
36
+ end
37
+
38
+ def og?() @og; end
39
+ def pen?() @pen; end
40
+
41
+ end # class Goal
42
+
43
+
44
+
45
+
46
+
47
+ =begin
48
+ ## sort by minutes
49
+ ## may not be sorted
50
+
51
+ recs = recs.sort do |l,r|
52
+ res = l[:minute] <=> r[:minute]
53
+ res = (l[:offset]||0) <=> (r[:offset]||0) if res == 0 &&
54
+ (l[:minute] == 45 ||
55
+ l[:minute] == 90 ||
56
+ l[:minute] == 105 || ## check - if possible stoppage in 1st half extra-time??
57
+ l[:minute] == 120)
58
+ res
59
+ end
60
+ =end
@@ -0,0 +1,76 @@
1
+
2
+
3
+ class Match
4
+
5
+
6
+ ##
7
+ ## use Match.build( h ) - why? why not?
8
+
9
+ attr_reader :team1, :team2,
10
+ :score,
11
+ :date_utc, :date_local, :utc_offset,
12
+ :stadium,
13
+ :goals1, :goals2
14
+
15
+
16
+
17
+ def initialize( doc, data )
18
+ ## expect a data as (json) hash for now
19
+ @doc = doc
20
+ @h = data
21
+
22
+
23
+ ## note - always lookup full team records (use match inline only as refs)
24
+ @team1 = @doc.find_team!( @h['team1'] )
25
+ @team2 = @doc.find_team!( @h['team2'] )
26
+
27
+
28
+ @date_utc = parse_date_utc( @h['datetime_utc'] )
29
+ @date_local = parse_date_local( @h['datetime_local'] )
30
+
31
+ assert( @date_utc.sec == 0 && @date_local.sec == 0,
32
+ "sec 00 expected" )
33
+
34
+ ## note: returns Rational (e.g. 3/1 or 1/4 etc.) use to_f/to_i to convert
35
+ ## diff_in_hours = ((localDateTime - dateTime) * 24).to_f
36
+ ## diff_in_days = localDateTime.jd - dateTime.jd
37
+
38
+ # note - offset is in rational fraction of a day (e.g. 1/12 for 2hours)
39
+ ## was diff_in_hours
40
+ @utc_offset = (@date_local.offset * 24).to_i
41
+
42
+ ## pp [@date_utc, @date_local, @utc_offset]
43
+
44
+ ## note - always lookup full stadium record (use match inline only as ref)
45
+ @stadium = @doc.find_stadium!( @h['stadium'] )
46
+
47
+
48
+ @score = @h['score'] ? Score.build( @h['score'] ) : nil
49
+
50
+
51
+ @goals1 = @h['goals1'] ? @h['goals1'].map { |h| Goal.build( h ) } : nil
52
+ @goals2 = @h['goals2'] ? @h['goals2'].map { |h| Goal.build( h ) } : nil
53
+ end
54
+
55
+
56
+
57
+ ## use _data to mark as "private" and do NOT use - why? why not?
58
+ def data() @h; end
59
+
60
+
61
+
62
+ ## todo - find a better name for timezone offset
63
+ ## use utc_offset - why? why not?
64
+ alias_method :diff_in_hours, :utc_offset
65
+
66
+
67
+ def stage() @h['stage']; end
68
+ def group() @h['group']; end # optional
69
+ def num() @h['number']; end # optional (match) number
70
+ def matchday() @h['matchday']; end # optional
71
+
72
+
73
+ def attendance() @h['attendance']; end
74
+
75
+
76
+ end ## class Match