<commit_before>##
# This module provides a consumable object based on Github's contribution stats

require 'curb'
require 'json'
require 'date'

##
# Rugged is used to check git's configuration for a github user, if available

begin
    require 'rugged'
    Using_Rugged = true
rescue LoadError
    Using_Rugged = false
end

##
# Basic_Cache is used to cache computation results, if available

begin
    require 'basic_cache'
    Using_Cache = true
rescue LoadError
    Using_Cache = false
end

module Github_Stats
    Version = '0.1.4'

    class << self
        ##
        # Add .new() helper for creating a new Contributions object

        def new(*args)
            self::Contributions.new(*args)
        end
    end

    ##
    # Datapoint objects provide a more sane date/time point interface

    Datapoint = Struct.new(:date, :score)

    ##
    # Contributions object holds and provides stats data for a user

    class Contributions
        attr_reader :user, :data, :last_updated
        attr_accessor :url

        ##
        # Create a new object
        # Guesses the user if one is not supplied
        # Automatically updates if not disabled
        # Creates a new cache object

        def initialize(user = nil, autoload=true)
            @user = user || guess_user
            @url = "https://github.com/users/#@user/contributions_calendar_data"
            @data = []
            @last_updated = nil
            @cache = Using_Cache ? Basic_Cache.new : Github_Stats::Null_Cache.new
            update if autoload
        end

        ##
        # Print human-readable string about object

        def to_s
            "Contributions from #@user"
        end
        alias :inspect :to_s

        ##
        # Return raw data directly

        def to_a
            @data
        end

        ##
        # Return Hash where keys are dates and values are scores

        def to_h
            @data.inject(Hash.new(0)) { |hash, point| hash[point.date] = point.score ; hash }
        end

        ##
        # Try to guess username
        # Checks Rugged if available, then ENV['USER']

        def guess_user
            names = []
            names << Rugged::Config.global['github.user'] if Using_Rugged
            names << ENV['USER']

            names.reject! {|name| name.nil? }
            names.length ? names.first : (raise "Failed to guess username")
        end

        ##
        # Update the data based on Github

        def update
            begin
                raw_data = JSON.parse Curl::Easy.perform(@url).body_str
            rescue
                raise 'Unable to load data from GitHub'
            end
            begin
                new_data = raw_data.collect { |date, score| Datapoint.new(Date::parse(date), score.to_i) }
            rescue
                raise "Failed to parse data"
            end
            @data = new_data
            @last_updated = DateTime.now
            @cache.clear
        end

        ##
        # Return today's score

        def today
            @cache.cache { to_h[Date.today] }
        end

        ##
        # Return the current streak

        def streak
            @cache.cache { @data.reverse.take_while{ |point| point.score > 0 } }
        end

        ##
        # Return the longest streak

        def longest_streak
            @cache.cache do
                @data.inject(Array.new(1, [])) do |streaks, point|
                    point.score == 0 ? streaks << [] : streaks.last << point ; streaks
                end.max {|a, b| a.length <=> b.length}
            end
        end

        ##
        # Return the highest score in the dataset

        def max
            @cache.cache { @data.max { |a, b| a.score <=> b.score } }
        end

        ##
        # Return the mean score in the dataset

        def mean
            @cache.cache { @data.inject(0){ |acc, x| x.score.to_f + acc } / @data.size }
        end

        ##
        # Return the break-points between the quartiles

        def quartile_boundaries(github = true)
            @cache.cache('quartile_boundaries' + (github ? '-github' : '')) do
                if github
                    magic_calc = gh_quartile_magic
                    data = @data.select { |x| (mean - x.score).abs / magic_calc <= 3.77972616981 }
                else
                    data = @data
                end
                range = data.map{ |p| p.score }.uniq.sort.select{ |s| not s.zero? }
                (1..3).map { |q| range[(q * range.length / 4) - 1] }.unshift(0).push(max.score)
            end
        end

        ##
        # Return the quartiles

        def quartiles(github = true)
            @cache.cache('quartiles' + (github ? '-github' : '')) do
                bounds = quartile_boundaries(github)
                groups = Array.new(5) { Array.new }
                @data.inject(groups) { |acc, point| acc[bounds.find_index{ |i| point.score <= i }] << point ; acc }
            end
        end
<commit_msg>offbyone<commit_after>##
# This module provides a consumable object based on Github's contribution stats

require 'curb'
require 'json'
require 'date'

##
# Rugged is used to check git's configuration for a github user, if available

begin
    require 'rugged'
    Using_Rugged = true
rescue LoadError
    Using_Rugged = false
end

##
# Basic_Cache is used to cache computation results, if available

begin
    require 'basic_cache'
    Using_Cache = true
rescue LoadError
    Using_Cache = false
end

module Github_Stats
    Version = '0.1.5'

    class << self
        ##
        # Add .new() helper for creating a new Contributions object

        def new(*args)
            self::Contributions.new(*args)
        end
    end

    ##
    # Datapoint objects provide a more sane date/time point interface

    Datapoint = Struct.new(:date, :score)

    ##
    # Contributions object holds and provides stats data for a user

    class Contributions
        attr_reader :user, :data, :last_updated
        attr_accessor :url

        ##
        # Create a new object
        # Guesses the user if one is not supplied
        # Automatically updates if not disabled
        # Creates a new cache object

        def initialize(user = nil, autoload=true)
            @user = user || guess_user
            @url = "https://github.com/users/#@user/contributions_calendar_data"
            @data = []
            @last_updated = nil
            @cache = Using_Cache ? Basic_Cache.new : Github_Stats::Null_Cache.new
            update if autoload
        end

        ##
        # Print human-readable string about object

        def to_s
            "Contributions from #@user"
        end
        alias :inspect :to_s

        ##
        # Return raw data directly

        def to_a
            @data
        end

        ##
        # Return Hash where keys are dates and values are scores

        def to_h
            @data.inject(Hash.new(0)) { |hash, point| hash[point.date] = point.score ; hash }
        end

        ##
        # Try to guess username
        # Checks Rugged if available, then ENV['USER']

        def guess_user
            names = []
            names << Rugged::Config.global['github.user'] if Using_Rugged
            names << ENV['USER']

            names.reject! {|name| name.nil? }
            names.length ? names.first : (raise "Failed to guess username")
        end

        ##
        # Update the data based on Github

        def update
            begin
                raw_data = JSON.parse Curl::Easy.perform(@url).body_str
            rescue
                raise 'Unable to load data from GitHub'
            end
            begin
                new_data = raw_data.collect { |date, score| Datapoint.new(Date::parse(date), score.to_i) }
            rescue
                raise "Failed to parse data"
            end
            @data = new_data
            @last_updated = DateTime.now
            @cache.clear
        end

        ##
        # Return today's score

        def today
            @cache.cache { to_h[Date.today] }
        end

        ##
        # Return the current streak

        def streak
            @cache.cache { @data.reverse.take_while{ |point| point.score > 0 } }
        end

        ##
        # Return the longest streak

        def longest_streak
            @cache.cache do
                @data.inject(Array.new(1, [])) do |streaks, point|
                    point.score == 0 ? streaks << [] : streaks.last << point ; streaks
                end.max {|a, b| a.length <=> b.length}
            end
        end

        ##
        # Return the highest score in the dataset

        def max
            @cache.cache { @data.max { |a, b| a.score <=> b.score } }
        end

        ##
        # Return the mean score in the dataset

        def mean
            @cache.cache { @data.inject(0){ |acc, x| x.score.to_f + acc } / @data.size }
        end

        ##
        # Return the break-points between the quartiles

        def quartile_boundaries(github = true)
            @cache.cache('quartile_boundaries' + (github ? '-github' : '')) do
                if github
                    magic_calc = gh_quartile_magic
                    data = @data.select { |x| (mean - x.score).abs / magic_calc <= 3.77972616981 }
                else
                    data = @data
                end
                range = data.map{ |p| p.score }.uniq.sort.select{ |s| not s.zero? }
                (1..3).map { |q| range[(q * range.length / 4) - 2] }.unshift(0).push(max.score)
            end
        end

        ##
        # Return the quartiles

        def quartiles(github = true)
            @cache.cache('quartiles' + (github ? '-github' : '')) do
                bounds = quartile_boundaries(github)
                groups = Array.new(5) { Array.new }
                @data.inject(groups) { |acc, point| acc[bounds.find_index{ |i| point.score <= i }] << point ; acc }
            end
        end

{'fraction_non_alphanumeric': 0.09593577521324637, 'fraction_numerical': 0.005218263923733066, 'mean_word_length': 1.5547295565239683, 'pattern_counts': {'":': 0, '<': 35, '<?xml version=': 0, '>': 9, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1313444', 'n_tokens_mistral': 2899, 'n_tokens_neox': 2726, 'n_words': 826}