├── .eslintignore ├── lib ├── zhong │ ├── version.rb │ ├── util.rb │ ├── every.rb │ ├── web.rb │ ├── web_helpers.rb │ ├── scheduler.rb │ ├── at.rb │ └── job.rb └── zhong.rb ├── bin ├── setup ├── console └── zhong ├── Gemfile ├── LICENSE.txt ├── Rakefile ├── .gitignore ├── .codeclimate.yml ├── web ├── assets │ └── javascript │ │ ├── application.js │ │ └── vendor.min.js └── views │ └── index.erb ├── .github └── workflows │ └── CI.yml ├── test ├── helper.rb ├── test_library.rb ├── test_job.rb ├── test_scheduler.rb ├── test_web.rb ├── test_every.rb └── test_at.rb ├── zhong.gemspec ├── CHANGELOG.md ├── .rubocop.yml ├── README.md └── .eslintrc /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /lib/zhong/version.rb: -------------------------------------------------------------------------------- 1 | module Zhong 2 | VERSION = "0.3.0".freeze 3 | end 4 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in zhong.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "zhong" 5 | 6 | require "irb" 7 | IRB.start 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Nick Elser 2 | 3 | Zhong is an Open Source project licensed under the terms of 4 | the LGPLv3 license. Please see 5 | for license text. 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.pattern = "test/**/test_*.rb" 7 | end 8 | 9 | task default: :test 10 | -------------------------------------------------------------------------------- /lib/zhong/util.rb: -------------------------------------------------------------------------------- 1 | module Zhong 2 | module Util 3 | def safe_mget(keys) 4 | if keys.empty? 5 | {} 6 | else 7 | Zhong.redis.mapped_mget(*keys) 8 | end 9 | end 10 | 11 | module_function :safe_mget 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem 3 | *.rbc 4 | .bundle 5 | .config 6 | .yardoc 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | zhong.rb 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | *.swp 21 | -------------------------------------------------------------------------------- /bin/zhong: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | STDERR.sync = STDOUT.sync = true 4 | 5 | require "zhong" 6 | 7 | file = ARGV.shift or abort "zhong " 8 | 9 | file = "./#{file}" unless file.match(/^[\/.]/) 10 | 11 | require file 12 | 13 | begin 14 | Zhong.start 15 | rescue => boom 16 | STDERR.puts boom.message 17 | STDERR.puts boom.backtrace.join("\n") 18 | exit 1 19 | end 20 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | eslint: 10 | enabled: true 11 | fixme: 12 | enabled: true 13 | rubocop: 14 | enabled: true 15 | ratings: 16 | paths: 17 | - "**.js" 18 | - "**.jsx" 19 | - "**.rb" 20 | exclude_paths: 21 | - "**/*helper*" 22 | - test/ 23 | - "**/*{.,-}min.js" 24 | -------------------------------------------------------------------------------- /web/assets/javascript/application.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $.timeago.settings.allowFuture = true; 3 | $.timeago.settings.refreshMillis = 0; 4 | $("time").timeago(); 5 | 6 | $(document).on("click", "[data-confirm]", function() { 7 | return confirm($(this).attr('data-confirm')); 8 | }); 9 | 10 | $(document).on("click", "[data-toggle]", function() { 11 | $($(this).attr('data-target')).toggle(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | ruby: 16 | - '2.5' 17 | - '2.6' 18 | - '2.7' 19 | - '3.0' 20 | - ruby-head 21 | continue-on-error: ${{ matrix.ruby == 'ruby-head' }} 22 | services: 23 | redis: 24 | image: redis 25 | ports: 26 | - 6379:6379 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby }} 32 | bundler-cache: true 33 | - run: | 34 | bundle exec rake 35 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | 3 | if ENV["CODECLIMATE_REPO_TOKEN"] 4 | require "codeclimate-test-reporter" 5 | ::SimpleCov.add_filter "helper" 6 | CodeClimate::TestReporter.start 7 | end 8 | 9 | require "zhong" 10 | require "minitest/autorun" 11 | require "rack/test" 12 | 13 | ENV["RACK_ENV"] = ENV["RAILS_ENV"] = "test" 14 | 15 | def assert_contains(expected_substring, string, *args) 16 | assert string.include?(expected_substring), *args 17 | end 18 | 19 | Zhong.logger = begin 20 | l = Logger.new(STDOUT) 21 | l.level = Logger::ERROR 22 | l 23 | end 24 | 25 | Zhong.redis = Redis.new(url: ENV["REDIS_URL"] || "redis://localhost/13") 26 | 27 | def test_default_config 28 | @default_config ||= { 29 | redis: Zhong.redis, 30 | logger: Zhong.logger, 31 | long_running_timeout: 10.seconds 32 | } 33 | end 34 | -------------------------------------------------------------------------------- /lib/zhong/every.rb: -------------------------------------------------------------------------------- 1 | module Zhong 2 | class Every 3 | class FailedToParse < StandardError; end 4 | 5 | EVERY_KEYWORDS = { 6 | minute: 1.minute, 7 | hour: 1.hour, 8 | day: 1.day, 9 | week: 1.week, 10 | month: 1.month, 11 | year: 1.year, 12 | decade: 10.years 13 | }.freeze 14 | 15 | def initialize(period) 16 | @period = period 17 | 18 | raise "`every` must be >= 1 second" unless valid? 19 | end 20 | 21 | def to_s 22 | EVERY_KEYWORDS.to_a.reverse.each do |friendly, period| 23 | next unless @period.to_i % period.to_i == 0 24 | 25 | rem = @period / period 26 | 27 | return "#{rem} #{friendly}" if rem == 1 28 | return "#{rem} #{friendly}s" 29 | end 30 | 31 | "#{@period.to_i} second#{@period.to_i == 1 ? '' : 's'}" 32 | end 33 | 34 | private def valid? 35 | @period.to_f >= 1 36 | end 37 | 38 | def next_at(last = Time.now) 39 | last + @period 40 | end 41 | 42 | def self.parse(every) 43 | case every 44 | when Numeric, ActiveSupport::Duration 45 | new(every) 46 | when String, Symbol 47 | key = every.downcase.to_sym 48 | 49 | raise FailedToParse, every unless EVERY_KEYWORDS.key?(key) 50 | 51 | new(EVERY_KEYWORDS[key]) 52 | else 53 | raise FailedToParse, every 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/test_library.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | class TestLibrary < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::Zhong::VERSION 6 | end 7 | 8 | def teardown 9 | Zhong.stop 10 | sleep 1 11 | end 12 | 13 | def test_logger 14 | test_logger = Zhong.logger 15 | Zhong.logger = nil 16 | assert_output(nil, nil) { Zhong.logger.info "ensure has default logger" } 17 | Zhong.logger = test_logger 18 | end 19 | 20 | def test_heartbeats 21 | Zhong.schedule { nil } 22 | t = Thread.new { Zhong.start } 23 | sleep(1) 24 | assert_equal true, Zhong.any_running? 25 | assert_in_delta Zhong.redis_time.to_f, Time.now.to_f, 1 26 | assert_in_delta Zhong.redis_time.to_f, Zhong.latest_heartbeat.to_f, 1 27 | refute_empty Zhong.all_heartbeats 28 | Zhong.stop 29 | t.join 30 | end 31 | 32 | def test_redis_change 33 | Zhong.schedule { nil } 34 | t = Thread.new { Zhong.start } 35 | sleep(1) 36 | assert_equal true, Zhong.any_running? 37 | test_redis = Zhong.redis 38 | Zhong.stop 39 | t.join 40 | Zhong.redis = Redis.new(url: "redis://localhost/15") 41 | refute Zhong.any_running?(5.seconds) 42 | t = Thread.new { Zhong.start } 43 | sleep(1) 44 | assert_equal true, Zhong.any_running? 45 | assert_in_delta Zhong.redis_time.to_f, Time.now.to_f, 1 46 | Zhong.stop 47 | Zhong.redis = test_redis 48 | t.join 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /zhong.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "zhong/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "zhong" 8 | spec.version = Zhong::VERSION 9 | spec.authors = ["Nick Elser"] 10 | spec.email = ["nick.elser@gmail.com"] 11 | 12 | spec.summary = %q{Reliable, distributed cron.} 13 | spec.description = %q{Reliable, distributed cron.} 14 | spec.homepage = "https://www.github.com/nickelser/zhong" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0") 18 | spec.bindir = "bin" 19 | spec.executables = ["zhong"] 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | spec.require_paths = ["lib"] 22 | 23 | spec.required_ruby_version = ">= 2.5.0" 24 | 25 | spec.add_dependency "suo" 26 | spec.add_dependency "redis" 27 | spec.add_dependency "tzinfo" 28 | spec.add_dependency "activesupport" 29 | 30 | spec.add_development_dependency "bundler", "~> 2.2" 31 | spec.add_development_dependency "rake", "~> 13.0" 32 | spec.add_development_dependency "rubocop", "~> 0.49.0" 33 | spec.add_development_dependency "minitest", "~> 5.5.0" 34 | spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4.7" 35 | spec.add_development_dependency "sinatra", "~> 1.4", ">= 1.4.6" 36 | spec.add_development_dependency "rack-test", "~> 0.6" 37 | spec.add_development_dependency "tilt" 38 | spec.add_development_dependency "erubis" 39 | spec.add_development_dependency "pry" 40 | end 41 | -------------------------------------------------------------------------------- /lib/zhong.rb: -------------------------------------------------------------------------------- 1 | require "digest" 2 | require "forwardable" 3 | require "logger" 4 | require "msgpack" 5 | require "redis" 6 | require "suo" 7 | require "active_support/time" 8 | 9 | require "zhong/version" 10 | require "zhong/util" 11 | 12 | require "zhong/every" 13 | require "zhong/at" 14 | 15 | require "zhong/job" 16 | require "zhong/scheduler" 17 | 18 | module Zhong 19 | class << self 20 | extend Forwardable 21 | attr_writer :logger, :redis, :heartbeat_key 22 | attr_accessor :tz 23 | 24 | def_delegators :scheduler, :start, :stop, :clear, :jobs, :redis_time 25 | end 26 | 27 | def self.schedule(&block) 28 | scheduler.instance_eval(&block) if block_given? 29 | end 30 | 31 | def self.scheduler 32 | @scheduler ||= Scheduler.new 33 | end 34 | 35 | def self.any_running?(grace = 60.seconds) 36 | latest_heartbeat && latest_heartbeat > (redis_time - grace) 37 | end 38 | 39 | def self.latest_heartbeat 40 | all_heartbeats.map { |h| h[:last_seen] }.sort.last 41 | end 42 | 43 | def self.all_heartbeats 44 | heartbeats = redis.hgetall(heartbeat_key) 45 | now = redis_time 46 | 47 | old_beats, new_beats = heartbeats.partition do |_, v| 48 | Time.at(v.to_i) < (now - 15.minutes) 49 | end 50 | 51 | redis.multi do 52 | old_beats.each { |b| redis.hdel(heartbeat_key, b) } 53 | end 54 | 55 | new_beats.map do |k, v| 56 | host, pid = k.split("#", 2) 57 | {host: host, pid: pid, last_seen: Time.at(v.to_i)} 58 | end 59 | end 60 | 61 | def self.logger 62 | @logger ||= Logger.new(STDOUT).tap do |logger| 63 | logger.formatter = -> (_, datetime, _, msg) { "#{datetime}: #{msg}\n" } 64 | end 65 | end 66 | 67 | def self.redis 68 | @redis ||= Redis.new(url: ENV["REDIS_URL"]) 69 | end 70 | 71 | def self.heartbeat_key 72 | @heartbeat_key ||= "zhong:heartbeat" 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 2 | 3 | - Update version requirements, and switch to Github actions (thank you @mlarraz!) 4 | 5 | ## 0.2.4 6 | 7 | - Compatibility with Sinatra/Tilt (thank you Brian Storti!) 8 | 9 | ## 0.2.3 10 | 11 | - Much improved documentation, and executable file naming (thank you Antoine Augusti!) 12 | - Fixes to the time parsing & to_s (thank you Antoine Augusti!) 13 | 14 | ## 0.2.2 15 | 16 | - Re-licensed as LGPL, as I lifted Sidekiq-web code to power Zhong web (thanks Mike Perham for the great gem and the code!) 17 | 18 | ## 0.2.1 19 | 20 | - Fix manually specifying a Redis connection (thanks, Richard Adams!) 21 | 22 | ## 0.2.0 23 | 24 | - Configuring Redis and the heartbeat key now correctly updates even after Zhong is configured initially. 25 | - Some cleanup in how config is stored in general. 26 | 27 | ## 0.1.9 28 | 29 | - Much more performant heartbeat checks (thanks, @sherinkurian). 30 | 31 | ## 0.1.8 32 | 33 | - Make it very clear when callbacks cause skips of ticks or runs. 34 | - Add logging when jobs/ticks are skipped. 35 | - Do not skip when callbacks return nil (only on false explicitly). 36 | 37 | ## 0.1.7 38 | 39 | - Improve test coverage. 40 | - Fix a small serialization issue. 41 | 42 | ## 0.1.6 43 | 44 | - Add Zhong.any_running? for monitoring that any Zhong node has checked in rencently 45 | - More code cleanup/refactoring. 46 | 47 | ## 0.1.5 48 | 49 | - Improve the API. 50 | - Add scheduler test. 51 | - Add Zhong::Web to see job status, and enable/disable jobs. Activate it with: 52 | Rails.application.routes.draw do 53 | # ... 54 | require "zhong/web" 55 | mount Zhong::Web => "/zhong" # or wherever 56 | 57 | ## 0.1.4 58 | 59 | - Fix several bugs related to time parsing. 60 | - In a totally unrelated change, add some tests around time parsing. 61 | 62 | ## 0.1.3 63 | 64 | - Fix several memory leaks. 65 | - Add a proper error handler block. 66 | 67 | ## 0.1.2 68 | 69 | - Fix bug with setting `at` like "**:35". 70 | 71 | ## 0.1.1 72 | 73 | - Handle multiple `at`s (at: ["mon 8:00, tues 9:30"]). 74 | - Job callbacks (:before_tick, :after_tick, etc). 75 | 76 | ## 0.1.0 77 | 78 | - First release. 79 | -------------------------------------------------------------------------------- /test/test_job.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | class TestJob < Minitest::Test 4 | def test_initialize 5 | job = Zhong::Job.new("test_initialize", {at: "12:00"}.merge(test_default_config)) 6 | 7 | assert job 8 | assert !job.running? 9 | assert_equal "test_initialize", job.to_s 10 | end 11 | 12 | def test_should_run 13 | sleep 1 14 | 15 | job = Zhong::Job.new("test_should_run", {every: 1.second}.merge(test_default_config)) 16 | 17 | assert_equal true, job.run? 18 | end 19 | 20 | def test_run 21 | success_counter = Queue.new 22 | job = Zhong::Job.new("test_run", {every: 1.second}.merge(test_default_config)) { success_counter << 1 } 23 | now = Time.now 24 | 25 | assert_equal 0, success_counter.size 26 | assert_equal true, job.run?(now) 27 | job.run(now) 28 | assert_equal false, job.run?(now) 29 | assert_equal 1, success_counter.size 30 | end 31 | 32 | def test_run_at 33 | success_counter = Queue.new 34 | job = Zhong::Job.new("test_run_at", {every: 1.second, at: ["**:**"]}.merge(test_default_config)) { success_counter << 1 } 35 | now = Time.now 36 | 37 | assert_equal 0, success_counter.size 38 | assert_equal true, job.run?(now) 39 | job.run(now) 40 | assert_equal false, job.run?(now) 41 | assert_equal 1, success_counter.size 42 | end 43 | 44 | def test_run_at_change 45 | success_counter = Queue.new 46 | job = Zhong::Job.new("test_run_at_change", {every: 1.second, at: ["**:**"]}.merge(test_default_config)) { success_counter << 1 } 47 | now = Time.now 48 | 49 | assert_equal 0, success_counter.size 50 | assert_equal true, job.run?(now) 51 | job.run(now) 52 | assert_equal false, job.run?(now) 53 | assert_equal 1, success_counter.size 54 | 55 | job = Zhong::Job.new("test_run_at_change", {every: 1.second, at: ["**:**", "**:**"]}.merge(test_default_config)) { success_counter << 1 } 56 | assert_equal true, job.run?(now) 57 | job.run(now) 58 | assert_equal false, job.run?(now) 59 | assert_equal 2, success_counter.size 60 | end 61 | 62 | def test_disable 63 | success_counter = Queue.new 64 | job = Zhong::Job.new("test_disable", {every: 1.second}.merge(test_default_config)) { success_counter << 1 } 65 | now = Time.now 66 | 67 | job.disable 68 | 69 | assert_equal true, job.run?(now) 70 | job.run(now) 71 | assert_equal true, job.run?(now) 72 | assert_equal 0, success_counter.size 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/zhong/web.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "erb" 3 | require "sinatra/base" 4 | 5 | require "zhong" 6 | require "zhong/web_helpers" 7 | 8 | module Zhong 9 | # Most of the following helpers are copied from a previous version of the Sidekiq project 10 | # available here: https://github.com/mperham/sidekiq/blob/2c9f7662fcdcb52d59b72ba0fe7dc5f963de4904/lib/sidekiq/web.rb 11 | class Web < Sinatra::Base 12 | enable :sessions 13 | use ::Rack::Protection, use: :authenticity_token unless ENV["RACK_ENV"] == "test" 14 | 15 | if ENV["ZHONG_WEB_USERNAME"] && ENV["ZHONG_WEB_PASSWORD"] 16 | # :nocov: 17 | use Rack::Auth::Basic, "Sorry." do |username, password| 18 | username == ENV["ZHONG_WEB_USERNAME"] and password == ENV["ZHONG_WEB_PASSWORD"] 19 | end 20 | # :nocov: 21 | end 22 | 23 | if ENV["RACK_ENV"] == "development" 24 | # :nocov: 25 | before do 26 | STDERR.puts "[params] #{params}" unless params.empty? 27 | end 28 | # :nocov: 29 | end 30 | 31 | set :root, File.expand_path(File.dirname(__FILE__) + "/../../web") 32 | set :public_folder, proc { "#{root}/assets" } 33 | set :views, proc { "#{root}/views" } 34 | 35 | helpers WebHelpers 36 | 37 | get "/" do 38 | index 39 | 40 | erb :index 41 | end 42 | 43 | post "/" do 44 | if params["disable"] 45 | job = Zhong.jobs[params["disable"]] 46 | 47 | job.disable if job 48 | elsif params["enable"] 49 | job = Zhong.jobs[params["enable"]] 50 | 51 | job.enable if job 52 | end 53 | 54 | index 55 | 56 | erb :index 57 | end 58 | 59 | def index 60 | @jobs = Zhong.jobs.values 61 | @last_runs = zhong_mget(@jobs, "last_ran") 62 | @disabled = zhong_mget(@jobs, "disabled") 63 | @hosts = Zhong.all_heartbeats 64 | end 65 | 66 | def zhong_mget(jobs, key) 67 | keys = jobs.map(&:to_s) 68 | ret = Zhong::Util.safe_mget(keys.map { |j| "zhong:#{key}:#{j}" }) 69 | Hash[keys.map { |j| [j, ret["zhong:#{key}:#{j}"]] }] 70 | end 71 | end 72 | end 73 | 74 | if defined?(::ActionDispatch::Request::Session) && !::ActionDispatch::Request::Session.respond_to?(:each) 75 | # mperham/sidekiq#2460 76 | # Rack apps can't reuse the Rails session store without 77 | # this monkeypatch 78 | class ActionDispatch::Request::Session 79 | # :nocov: 80 | def each(&block) 81 | hash = self.to_hash 82 | hash.each(&block) 83 | end 84 | # :nocov: 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/test_scheduler.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | class TestScheduler < Minitest::Test 4 | def setup 5 | Zhong.clear 6 | end 7 | 8 | def test_scheduler 9 | test_one_counter = 0 10 | test_two_counter = 0 11 | 12 | Zhong.schedule do 13 | every(10.seconds, "test_one") { test_one_counter += 1 } 14 | every(3.seconds, "test_two") { test_two_counter += 1 } 15 | end 16 | 17 | t = Thread.new { Zhong.start } 18 | sleep(7) 19 | Zhong.stop 20 | t.join 21 | 22 | assert_equal 1, test_one_counter 23 | assert_equal 3, test_two_counter 24 | end 25 | 26 | def test_scheduler_categories 27 | Zhong.schedule do 28 | category "cat1" do 29 | every(10.seconds, "cat_test_one") { nil } 30 | end 31 | end 32 | 33 | assert_equal 1, Zhong.jobs.size 34 | assert_equal "cat1.cat_test_one", Zhong.jobs.values.first.to_s 35 | end 36 | 37 | def test_scheduler_nested_category 38 | assert_raises RuntimeError do 39 | Zhong.schedule do 40 | category "cat1" do 41 | category "cat2" 42 | end 43 | end 44 | end 45 | end 46 | 47 | def test_duplicate_job_name 48 | boom = assert_raises RuntimeError do 49 | Zhong.schedule do 50 | every(1.second, "dupe_job") { nil } 51 | every(1.second, "dupe_job") { nil } 52 | end 53 | end 54 | 55 | assert_match(/duplicate job dupe_job/, boom.message) 56 | end 57 | 58 | def test_scheduler_callbacks 59 | test_before_tick = 0 60 | test_after_tick = 0 61 | test_errors = 0 62 | test_before_run = 0 63 | test_after_run = 0 64 | 65 | Zhong.schedule do 66 | on(:before_tick) { test_before_tick += 1; true } 67 | on(:after_tick) { test_after_tick += 1 } 68 | on(:before_run) { test_before_run += 1; true } 69 | on(:after_run) { |_, _, ran| test_after_run += 1 if ran } 70 | error_handler { test_errors += 1 } 71 | 72 | every(1.second, "test_every_second") { nil } 73 | every(1.second, "break_every_second") { raise "boom" } 74 | end 75 | 76 | t = Thread.new { Zhong.start } 77 | sleep(3) 78 | Zhong.stop 79 | t.join 80 | 81 | assert_operator 2, :<=, test_before_tick 82 | assert_operator 4, :>=, test_before_tick 83 | assert_operator 2, :<=, test_after_tick 84 | assert_operator 4, :>=, test_after_tick 85 | assert_operator 6, :<=, test_before_run 86 | assert_operator 8, :>=, test_before_run 87 | assert_operator 2, :<=, test_after_run 88 | assert_operator 4, :>=, test_after_run 89 | assert_operator 2, :<=, test_errors 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/zhong/web_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "uri" 3 | 4 | module Zhong 5 | # Most of the following helpers are copied from a previous version of the Sidekiq project 6 | # available here: https://github.com/mperham/sidekiq/blob/2c9f7662fcdcb52d59b72ba0fe7dc5f963de4904/lib/sidekiq/web_helpers.rb 7 | module WebHelpers 8 | # Simple capture method for erb templates. The origin was 9 | # capture method from sinatra-contrib library. 10 | def capture(&block) 11 | block.call 12 | eval("", block.binding) 13 | end 14 | 15 | def root_path 16 | "#{env['SCRIPT_NAME']}/" 17 | end 18 | 19 | def current_path 20 | @current_path ||= request.path_info.gsub(%r(^\/),"") 21 | end 22 | 23 | def relative_time(time) 24 | if time 25 | %() 26 | else 27 | "never" 28 | end 29 | end 30 | 31 | def truncate(text, truncate_after_chars = 2000) 32 | truncate_after_chars && text.size > truncate_after_chars ? "#{text[0..truncate_after_chars]}..." : text 33 | end 34 | 35 | def display_args(args, truncate_after_chars = 2000) 36 | args.map do |arg| 37 | h(truncate(to_display(arg), truncate_after_chars)) 38 | end.join(", ") 39 | end 40 | 41 | def csrf_tag 42 | "" 43 | end 44 | 45 | def to_display(arg) 46 | arg.inspect 47 | rescue 48 | begin 49 | arg.to_s 50 | rescue => ex 51 | "Cannot display argument: [#{ex.class.name}] #{ex.message}" 52 | end 53 | end 54 | 55 | def number_with_delimiter(number) 56 | begin 57 | Float(number) 58 | rescue ArgumentError, TypeError 59 | return number 60 | end 61 | 62 | options = {delimiter: ",", separator: "."} 63 | parts = number.to_s.to_str.split(".") 64 | parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") 65 | parts.join(options[:separator]) 66 | end 67 | 68 | def h(text) 69 | ::Rack::Utils.escape_html(text) 70 | rescue ArgumentError => e 71 | raise unless e.message.eql?("invalid byte sequence in UTF-8") 72 | text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16") 73 | retry 74 | end 75 | 76 | def environment_title_prefix 77 | environment = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" 78 | 79 | "[#{environment.upcase}] " unless environment == "production" 80 | end 81 | 82 | def product_version 83 | "Zhong v#{Zhong::VERSION}" 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/test_web.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | require "zhong/web" 3 | require "tilt/erubis" 4 | 5 | class TestWeb < Minitest::Test 6 | include Rack::Test::Methods 7 | 8 | def app 9 | Zhong::Web 10 | end 11 | 12 | def setup 13 | Zhong.clear 14 | end 15 | 16 | def test_index 17 | get "/" 18 | assert last_response.ok? 19 | assert_contains "[TEST] Zhong", last_response.body 20 | assert_contains Zhong::VERSION, last_response.body 21 | end 22 | 23 | def test_index_job 24 | Zhong.schedule do 25 | every(10.minutes, "test_web_job") { nil } 26 | end 27 | 28 | get "/" 29 | assert last_response.ok? 30 | assert_contains "test_web_job", last_response.body 31 | assert_contains "every 10 minutes", last_response.body 32 | end 33 | 34 | def test_disable_job 35 | test_before_disable = 0 36 | test_after_disable = 0 37 | 38 | Zhong.schedule do 39 | every(30.seconds, "test_disable_web_job") { nil } 40 | on(:before_disable) { test_before_disable += 1 } 41 | on(:after_disable) { test_after_disable += 1 } 42 | end 43 | 44 | job = Zhong.scheduler.find_by_name("test_disable_web_job") 45 | 46 | job.enable 47 | 48 | assert_equal false, job.disabled? 49 | 50 | post "/", "disable" => job.id 51 | assert last_response.ok? 52 | assert_contains "test_disable_web_job", last_response.body 53 | assert_contains "every 30 seconds", last_response.body 54 | assert_contains 'name="enable"', last_response.body 55 | assert_equal true, job.disabled? 56 | assert_equal 1, test_before_disable 57 | assert_equal 1, test_after_disable 58 | end 59 | 60 | def test_enable_job 61 | test_before_enable = 0 62 | test_after_enable = 0 63 | 64 | Zhong.schedule do 65 | every(12.hours, "test_enable_web_job") { nil } 66 | on(:before_enable) { test_before_enable += 1 } 67 | on(:after_enable) { test_after_enable += 1 } 68 | end 69 | 70 | job = Zhong.scheduler.find_by_name("test_enable_web_job") 71 | 72 | job.disable 73 | 74 | assert_equal true, job.disabled? 75 | 76 | post "/", "enable" => job.id 77 | assert last_response.ok? 78 | assert_contains "test_enable_web_job", last_response.body 79 | assert_contains "every 12 hours", last_response.body 80 | assert_contains 'name="disable"', last_response.body 81 | assert_equal false, job.disabled? 82 | assert_equal 1, test_before_enable 83 | assert_equal 1, test_after_enable 84 | end 85 | 86 | def test_heartbeat 87 | hostname = `hostname`.strip 88 | pid = Process.pid 89 | 90 | t = Thread.new { Zhong.start } 91 | sleep(1) 92 | Zhong.stop 93 | t.join 94 | 95 | get "/" 96 | assert last_response.ok? 97 | assert_contains hostname, last_response.body 98 | assert_contains "PID #{pid}", last_response.body 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /web/views/index.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= environment_title_prefix %>Zhong 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 57 | 58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | <% @jobs.each do |job| %> 72 | <% enabled = !@disabled[job.to_s] %> 73 | <% enabled_str = enabled ? 'disable' : 'enable' %> 74 | <% last_run = @last_runs[job.to_s] %> 75 | "> 76 | 77 | 85 | 86 | 87 | 95 | 96 | <% end %> 97 | 98 |
JobScheduledLast RunNext RunAction
<%= h(job) %> 78 | <% if job.every %> 79 | every <%= job.every %> 80 | <% end %> 81 | <% if job.at %> 82 | at <%= job.at %> 83 | <% end %> 84 | <%= relative_time(last_run ? Time.at(last_run.to_i) : nil) %><%= relative_time(job.next_at) %> 88 |
89 | <%= csrf_tag %> 90 |
91 | 92 |
93 |
94 |
99 |
100 |
101 |

Hosts

102 |
    103 | <% @hosts.each do |host| %> 104 |
  • 105 | <%= h(host[:host]) %> (PID <%= host[:pid] %>): last seen <%= relative_time(host[:last_seen]) %>. 106 |
  • 107 | <% end %> 108 |
109 |
110 |
111 | <%= product_version %> 112 |
113 | 114 | 115 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - .git/**/* 4 | - tmp/**/* 5 | - zhong.gemspec 6 | 7 | Lint/DuplicateMethods: 8 | Enabled: true 9 | 10 | Lint/DeprecatedClassMethods: 11 | Enabled: true 12 | 13 | Style/TrailingWhitespace: 14 | Enabled: true 15 | 16 | Style/Tab: 17 | Enabled: true 18 | 19 | Style/TrailingBlankLines: 20 | Enabled: true 21 | 22 | Style/NilComparison: 23 | Enabled: true 24 | 25 | Style/NonNilCheck: 26 | Enabled: true 27 | 28 | Style/Not: 29 | Enabled: true 30 | 31 | Style/RedundantReturn: 32 | Enabled: true 33 | 34 | Style/ClassCheck: 35 | Enabled: true 36 | 37 | Style/EmptyLines: 38 | Enabled: true 39 | 40 | Style/EmptyLiteral: 41 | Enabled: true 42 | 43 | Style/Alias: 44 | Enabled: true 45 | 46 | Style/MethodCallParentheses: 47 | Enabled: true 48 | 49 | Style/MethodDefParentheses: 50 | Enabled: true 51 | 52 | Style/SpaceBeforeBlockBraces: 53 | Enabled: true 54 | 55 | Style/SpaceInsideBlockBraces: 56 | Enabled: true 57 | 58 | Style/SpaceInsideParens: 59 | Enabled: true 60 | 61 | Style/DeprecatedHashMethods: 62 | Enabled: true 63 | 64 | Style/HashSyntax: 65 | Enabled: true 66 | 67 | Style/SpaceInsideHashLiteralBraces: 68 | Enabled: true 69 | EnforcedStyle: no_space 70 | 71 | Style/SpaceInsideBrackets: 72 | Enabled: true 73 | 74 | Style/AndOr: 75 | Enabled: false 76 | 77 | Style/TrailingCommaInLiteral: 78 | Enabled: true 79 | 80 | Style/SpaceBeforeComma: 81 | Enabled: true 82 | 83 | Style/SpaceBeforeComment: 84 | Enabled: true 85 | 86 | Style/SpaceBeforeSemicolon: 87 | Enabled: true 88 | 89 | Style/SpaceAroundBlockParameters: 90 | Enabled: true 91 | 92 | Style/SpaceAroundOperators: 93 | Enabled: true 94 | 95 | Style/SpaceAfterColon: 96 | Enabled: true 97 | 98 | Style/SpaceAfterComma: 99 | Enabled: true 100 | 101 | Style/SpaceAroundKeyword: 102 | Enabled: true 103 | 104 | Style/SpaceAfterNot: 105 | Enabled: true 106 | 107 | Style/SpaceAfterSemicolon: 108 | Enabled: true 109 | 110 | Lint/UselessComparison: 111 | Enabled: true 112 | 113 | Lint/InvalidCharacterLiteral: 114 | Enabled: true 115 | 116 | Lint/LiteralInInterpolation: 117 | Enabled: true 118 | 119 | Lint/LiteralInCondition: 120 | Enabled: true 121 | 122 | Lint/UnusedBlockArgument: 123 | Enabled: true 124 | 125 | Style/VariableInterpolation: 126 | Enabled: true 127 | 128 | Style/RedundantSelf: 129 | Enabled: true 130 | 131 | Style/ParenthesesAroundCondition: 132 | Enabled: true 133 | 134 | Style/WhileUntilDo: 135 | Enabled: true 136 | 137 | Style/EmptyLineBetweenDefs: 138 | Enabled: true 139 | 140 | Style/EmptyLinesAroundAccessModifier: 141 | Enabled: true 142 | 143 | Style/EmptyLinesAroundMethodBody: 144 | Enabled: true 145 | 146 | Style/ColonMethodCall: 147 | Enabled: true 148 | 149 | Lint/SpaceBeforeFirstArg: 150 | Enabled: true 151 | 152 | Lint/UnreachableCode: 153 | Enabled: true 154 | 155 | Style/UnlessElse: 156 | Enabled: true 157 | 158 | Style/ClassVars: 159 | Enabled: true 160 | 161 | Style/StringLiterals: 162 | Enabled: true 163 | EnforcedStyle: double_quotes 164 | 165 | Metrics/CyclomaticComplexity: 166 | Max: 10 167 | 168 | Metrics/LineLength: 169 | Max: 128 170 | 171 | Metrics/MethodLength: 172 | Max: 32 173 | 174 | Metrics/PerceivedComplexity: 175 | Max: 8 176 | 177 | # Disabled 178 | 179 | Style/EvenOdd: 180 | Enabled: false 181 | 182 | Style/AsciiComments: 183 | Enabled: false 184 | 185 | Style/NumericLiterals: 186 | Enabled: false 187 | 188 | Style/UnneededPercentQ: 189 | Enabled: false 190 | 191 | Style/SpecialGlobalVars: 192 | Enabled: false 193 | 194 | Style/TrivialAccessors: 195 | Enabled: false 196 | 197 | Style/PerlBackrefs: 198 | Enabled: false 199 | 200 | Metrics/AbcSize: 201 | Enabled: false 202 | 203 | Metrics/BlockNesting: 204 | Enabled: false 205 | 206 | Metrics/ClassLength: 207 | Enabled: false 208 | 209 | Metrics/MethodLength: 210 | Enabled: false 211 | 212 | Metrics/ParameterLists: 213 | Enabled: false 214 | 215 | Metrics/PerceivedComplexity: 216 | Enabled: false 217 | 218 | Style/Documentation: 219 | Enabled: false 220 | -------------------------------------------------------------------------------- /lib/zhong/scheduler.rb: -------------------------------------------------------------------------------- 1 | module Zhong 2 | class Scheduler 3 | extend Forwardable 4 | 5 | def_delegators Zhong, :redis, :tz, :logger, :heartbeat_key 6 | attr_reader :jobs 7 | 8 | DEFAULT_CONFIG = { 9 | timeout: 0.5, 10 | grace: 15.minutes, 11 | long_running_timeout: 5.minutes 12 | }.freeze 13 | 14 | def initialize(config = {}) 15 | @jobs = {} 16 | @callbacks = {} 17 | @config = DEFAULT_CONFIG.merge(config) 18 | 19 | @category = nil 20 | @error_handler = nil 21 | @running = false 22 | end 23 | 24 | def clear 25 | raise "unable to clear while running; run Zhong.stop first" if @running 26 | 27 | @jobs = {} 28 | @callbacks = {} 29 | @category = nil 30 | end 31 | 32 | def category(name) 33 | raise "cannot nest categories: #{name} would be nested in #{@category} (#{caller.first})" if @category 34 | 35 | @category = name.to_s 36 | 37 | yield(self) 38 | 39 | @category = nil 40 | end 41 | 42 | def every(period, name, opts = {}, &block) 43 | raise "must specify a period for #{name} (#{caller.first})" unless period 44 | 45 | job = Job.new(name, opts.merge(@config).merge(every: period, category: @category), @callbacks, &block) 46 | 47 | raise "duplicate job #{job}" if jobs.key?(job.id) 48 | 49 | @jobs[job.id] = job 50 | end 51 | 52 | def error_handler(&block) 53 | @error_handler = block if block_given? 54 | @error_handler 55 | end 56 | 57 | def on(event, &block) 58 | unless [:before_tick, :after_tick, :before_run, :after_run, :before_disable, 59 | :after_disable, :before_enable, :after_enable].include?(event.to_sym) 60 | raise "unknown callback #{event}" 61 | end 62 | (@callbacks[event.to_sym] ||= []) << block 63 | end 64 | 65 | def start 66 | logger.info "starting at #{redis_time}" 67 | 68 | @stop = false 69 | 70 | trap_signals 71 | 72 | raise "already running" if @running 73 | 74 | loop do 75 | @running = true 76 | 77 | if fire_callbacks(:before_tick) 78 | now = redis_time 79 | 80 | jobs_to_run(now).each do |_, job| 81 | break if @stop 82 | run_job(job, now) 83 | end 84 | 85 | break if @stop 86 | 87 | fire_callbacks(:after_tick) 88 | 89 | heartbeat(now) 90 | 91 | break if @stop 92 | else 93 | logger.info "skipping tick due to a `:before_tick` callback" 94 | end 95 | 96 | sleep_until_next_second 97 | 98 | break if @stop 99 | end 100 | 101 | @running = false 102 | 103 | Thread.new { logger.info "stopped" }.join 104 | end 105 | 106 | def stop 107 | Thread.new { logger.error "stopping" } if @running # thread necessary due to trap context 108 | @stop = true 109 | end 110 | 111 | def find_by_name(job_name) 112 | @jobs[Digest::SHA256.hexdigest(job_name)] 113 | end 114 | 115 | def redis_time 116 | s, ms = redis.time # returns [seconds since epoch, microseconds] 117 | now = Time.at(s + ms / (10**6)) 118 | tz ? now.in_time_zone(tz) : now 119 | end 120 | 121 | private 122 | 123 | TRAPPED_SIGNALS = %w(QUIT INT TERM).freeze 124 | private_constant :TRAPPED_SIGNALS 125 | 126 | def fire_callbacks(event, *args) 127 | @callbacks[event].to_a.map do |callback| 128 | callback.call(*args) 129 | end.compact.all? # do not skip on nils 130 | end 131 | 132 | def jobs_to_run(time = redis_time) 133 | jobs.select { |_, job| job.run?(time) } 134 | end 135 | 136 | def run_job(job, time = redis_time) 137 | unless fire_callbacks(:before_run, job, time) 138 | logger.info "skipping #{job} due to a `:before_run` callback" 139 | return 140 | end 141 | 142 | ran = job.run(time, error_handler) 143 | 144 | fire_callbacks(:after_run, job, time, ran) 145 | end 146 | 147 | def heartbeat(time) 148 | redis.hset(heartbeat_key, heartbeat_field, time.to_i) 149 | end 150 | 151 | def heartbeat_field 152 | @heartbeat_field ||= "#{`hostname`.strip}##{Process.pid}" 153 | end 154 | 155 | def trap_signals 156 | TRAPPED_SIGNALS.each do |sig| 157 | Signal.trap(sig) { stop } 158 | end 159 | end 160 | 161 | def sleep_until_next_second 162 | GC.start 163 | sleep(1.0 - Time.now.subsec + 0.0001) 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zhong [![Build Status](https://github.com/nickelser/zhong/workflows/CI/badge.svg)](https://github.com/nickelser/zhong/actions?query=workflow%3ACI) [![Code Climate](https://codeclimate.com/github/nickelser/zhong/badges/gpa.svg)](https://codeclimate.com/github/nickelser/zhong) [![Gem Version](https://badge.fury.io/rb/zhong.svg)](http://badge.fury.io/rb/zhong) 2 | 3 | Useful, reliable distributed cron. Tired of your cron-like scheduler running key jobs twice? Would you like to be able to run your cron server on multiple machines and have it "just work"? Have we got the gem for you. 4 | 5 | Zhong uses Redis to acquire exclusive locks on jobs, as well as recording when they last ran. This means that you can rest easy at night, knowing that your customers are getting their monthly Goat Fancy magazine subscriptions and you are rolling around in your piles of money without a care in the world. 6 | 7 | :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) 8 | # Installation 9 | 10 | Add this line to your application’s Gemfile: 11 | 12 | ```ruby 13 | gem 'zhong' 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### Zhong schedule 19 | Create a definition file, let's call it `zhong.rb`: 20 | 21 | ```ruby 22 | Zhong.redis = Redis.new(url: ENV["ZHONG_REDIS_URL"]) 23 | 24 | Zhong.schedule do 25 | category "stuff" do 26 | every 5.seconds, "foo" do 27 | puts "foo" 28 | end 29 | 30 | every(1.minute, "running biz at 26th and 27th minute", at: ["**:26", "**:27"]) { puts "biz" } 31 | every(1.week, "running baz on mon and wed", at: ["mon 22:45", "wed 23:13"]) { puts "baz" } 32 | every(10.seconds, "boom every 10 seconds") { raise "fail" } 33 | end 34 | 35 | category "clutter" do 36 | every(1.second, "compute", if: -> (t) { t.wday == 3 && rand < 0.5 }) do 37 | puts "something happened on wednesday, maybe" 38 | end 39 | end 40 | 41 | # note: callbacks that explicitly false will cause event to not run 42 | on(:before_tick) do 43 | puts "ding" 44 | true 45 | end 46 | 47 | on(:after_tick) do 48 | puts "dong" 49 | end 50 | 51 | on(:before_run) do |job, time| 52 | puts "running #{job}" 53 | true # can conditionally run a specific job 54 | end 55 | 56 | on(:after_run) do |job, time, ran| 57 | puts "#{job} ran?: #{ran}" 58 | end 59 | 60 | on(:before_disable) do |job| 61 | puts "#{job} is going to be disabled" 62 | end 63 | 64 | on(:after_disable) do |job| 65 | puts "#{job} disabled" 66 | end 67 | 68 | on(:before_enable) do |job| 69 | puts "#{job} is going to be enabled" 70 | end 71 | 72 | on(:after_enable) do |job| 73 | puts "#{job} enabled" 74 | end 75 | 76 | error_handler do |e, job| 77 | puts "dang, #{job} messed up: #{e}" 78 | end 79 | end 80 | ``` 81 | 82 | This file only describes what should be the schedule. Nothing will be executed 83 | until we actually run 84 | ```ruby 85 | Zhong.start 86 | ``` 87 | after describing the Zhong schedule. 88 | 89 | ### Zhong cron process 90 | 91 | You can run the cron process that will execute your code from the definitions 92 | in the `zhong.rb` file by running: 93 | ```sh 94 | zhong zhong.rb 95 | ``` 96 | 97 | ## Web UI 98 | 99 | Zhong comes with a web application that can display jobs, their last run and 100 | enable/disable them. 101 | 102 | This is a Sinatra application that requires at least `v2.0.0`. You can add to your Gemfile 103 | ```ruby 104 | gem 'sinatra', "~>2.0" 105 | ``` 106 | 107 | It can be protected by HTTP basic authentication by 108 | setting the following environment variables: 109 | - `ZHONG_WEB_USERNAME`: the username 110 | - `ZHONG_WEB_PASSWORD`: the password 111 | 112 | You'll need to load the Zhong schedule to be able to see jobs in the web UI, typically 113 | by requiring your `zhong.rb` definition file. 114 | 115 | ### Rails 116 | Load the Zhong schedule by creating an initializer at `config/initializers/zhong.rb`, 117 | with the following content: 118 | ```ruby 119 | require "#{Rails.root}/zhong.rb" 120 | ``` 121 | 122 | Add the following to your `config/routes.rb`: 123 | ```ruby 124 | require 'zhong/web' 125 | 126 | Rails.application.routes.draw do 127 | # Other routes here... 128 | 129 | mount Zhong::Web, at: "/zhong" 130 | end 131 | ``` 132 | 133 | ## History 134 | 135 | View the [changelog](https://github.com/nickelser/zhong/blob/master/CHANGELOG.md). 136 | 137 | ## Contributing 138 | 139 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 140 | 141 | - [Report bugs](https://github.com/nickelser/zhong/issues) 142 | - Fix bugs and [submit pull requests](https://github.com/nickelser/zhong/pulls) 143 | - Write, clarify, or fix documentation 144 | - Suggest or add new features 145 | -------------------------------------------------------------------------------- /lib/zhong/at.rb: -------------------------------------------------------------------------------- 1 | module Zhong 2 | # Strongly inspired by the Clockwork At class 3 | class At 4 | class FailedToParse < StandardError; end 5 | 6 | WDAYS = %w(sunday monday tuesday wednesday thursday friday saturday).each.with_object({}).with_index do |(w, wdays), index| 7 | [w, w[0...3]].each do |k| 8 | wdays[k] = index 9 | 10 | if k == "tue" 11 | wdays["tues"] = index 12 | elsif k == "thu" 13 | wdays["thr"] = index 14 | end 15 | end 16 | end.freeze 17 | 18 | attr_accessor :minute, :hour, :wday 19 | 20 | def self.parse(at, grace: 0.seconds) 21 | if at.respond_to?(:each) 22 | MultiAt.new(at.map { |a| parse_at(a, grace) }) 23 | else 24 | parse_at(at, grace) 25 | end 26 | rescue ArgumentError 27 | raise FailedToParse, at 28 | end 29 | 30 | def self.deserialize(at) 31 | parse_serialized(MessagePack.unpack(at)) 32 | end 33 | 34 | def initialize(minute: nil, hour: nil, wday: nil, grace: 0.seconds) 35 | @minute = minute 36 | @hour = hour 37 | @wday = wday 38 | @grace = grace 39 | 40 | raise ArgumentError unless valid? 41 | end 42 | 43 | def next_at(time = Time.now) 44 | at_time = at_time_day_hour_minute_adjusted(time) 45 | 46 | grace_cutoff = time.change(sec: 0) - @grace 47 | 48 | if at_time < grace_cutoff 49 | at_time + if @wday.nil? 50 | @hour.nil? ? 1.hour : 1.day 51 | else 52 | 1.week 53 | end 54 | else 55 | at_time 56 | end 57 | end 58 | 59 | def to_s 60 | str = "#{formatted_time(@hour)}:#{formatted_time(@minute)}" 61 | str += " on #{WDAYS.invert[@wday].capitalize}" if @wday 62 | 63 | str 64 | end 65 | 66 | def as_json 67 | {m: @minute, h: @hour, w: @wday, g: @grace} 68 | end 69 | 70 | def serialize 71 | MessagePack.pack(as_json) 72 | end 73 | 74 | def ==(other) 75 | other.class == self.class && other.state == state 76 | end 77 | 78 | def self.parse_serialized(at) 79 | if at.is_a?(Array) 80 | MultiAt.new(at.map { |a| parse_serialized(a) }) 81 | else 82 | new(minute: at["m"], hour: at["h"], wday: at["w"], grace: at["g"]) 83 | end 84 | end 85 | private_class_method :parse_serialized 86 | 87 | def self.parse_at(at, grace) 88 | case at 89 | when /\A([[:alpha:]]+)\s+(.*)\z/ 90 | wday = WDAYS[$1.downcase] 91 | 92 | raise FailedToParse, at unless wday 93 | 94 | parsed_time = parse_at($2, grace) 95 | parsed_time.wday = wday 96 | parsed_time 97 | when /\A(\d{1,2}):(\d\d)\z/ 98 | new(minute: $2.to_i, hour: $1.to_i, grace: grace) 99 | when /\A\*{1,2}:(\d\d)\z/ 100 | new(minute: $1.to_i, grace: grace) 101 | when /\A(\d{1,2}):\*{1,2}\z/ 102 | new(hour: $1.to_i, grace: grace) 103 | when /\A\*{1,2}:\*{1,2}\z/ 104 | new(grace: grace) 105 | else 106 | raise FailedToParse, at 107 | end 108 | end 109 | private_class_method :parse_at 110 | 111 | protected 112 | 113 | def formatted_time(t) 114 | if t.nil? 115 | "**" 116 | else 117 | t.to_s.rjust(2, "0") 118 | end 119 | end 120 | 121 | def state 122 | [@minute, @hour, @wday] 123 | end 124 | 125 | private 126 | 127 | def at_time_hour_minute_adjusted(time) 128 | if @minute && @hour 129 | time.change(hour: @hour, min: @minute) 130 | elsif @minute 131 | time.change(min: @minute) 132 | elsif @hour && @hour != time.hour 133 | time.change(hour: @hour) 134 | else 135 | time.change(sec: 0) 136 | end 137 | end 138 | 139 | def at_time_day_hour_minute_adjusted(time) 140 | at_time_hour_minute_adjusted(time) + (@wday ? (@wday - time.wday) : 0).days 141 | end 142 | 143 | def valid? 144 | (@minute.nil? || (0..59).cover?(@minute)) && 145 | (@hour.nil? || (0..23).cover?(@hour)) && 146 | (@wday.nil? || (0..6).cover?(@wday)) 147 | end 148 | end 149 | 150 | class MultiAt 151 | attr_accessor :ats 152 | 153 | def initialize(ats = []) 154 | @ats = ats 155 | end 156 | 157 | def ==(other) 158 | other.class == self.class && @ats == other.ats 159 | end 160 | 161 | def next_at(time = Time.now) 162 | ats.map { |at| at.next_at(time) }.min 163 | end 164 | 165 | def to_s 166 | ats.map(&:to_s).join(", ") 167 | end 168 | 169 | def as_json 170 | ats.map(&:as_json) 171 | end 172 | 173 | def serialize 174 | MessagePack.pack(as_json) 175 | end 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /test/test_every.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | class TestEvery < Minitest::Test 4 | def time_in_day(hour, minute, day = 0, sec = 0) 5 | Time.new.change(hour: hour, min: minute, sec: sec) + day.days 6 | end 7 | 8 | def test_numeric_every_10s 9 | every = Zhong::Every.parse(10) 10 | 11 | assert_equal time_in_day(16, 20, 0, 10), every.next_at(time_in_day(16, 20)) 12 | assert_equal time_in_day(16, 20, 0, 20), every.next_at(time_in_day(16, 20, 0, 10)) 13 | assert_equal time_in_day(16, 20, 0, 25), every.next_at(time_in_day(16, 20, 0, 15)) 14 | assert_equal time_in_day(0, 0, 1, 9), every.next_at(time_in_day(23, 59, 0, 59)) 15 | end 16 | 17 | def test_numeric_every_30s 18 | every = Zhong::Every.parse(30) 19 | 20 | assert_equal time_in_day(16, 20, 0, 30), every.next_at(time_in_day(16, 20)) 21 | assert_equal time_in_day(16, 20, 0, 40), every.next_at(time_in_day(16, 20, 0, 10)) 22 | assert_equal time_in_day(16, 20, 0, 45), every.next_at(time_in_day(16, 20, 0, 15)) 23 | assert_equal time_in_day(0, 0, 1, 29), every.next_at(time_in_day(23, 59, 0, 59)) 24 | end 25 | 26 | def test_duration_30s 27 | every = Zhong::Every.parse(30.seconds) 28 | 29 | assert_equal time_in_day(16, 20, 0, 30), every.next_at(time_in_day(16, 20)) 30 | assert_equal time_in_day(16, 20, 0, 40), every.next_at(time_in_day(16, 20, 0, 10)) 31 | assert_equal time_in_day(16, 20, 0, 45), every.next_at(time_in_day(16, 20, 0, 15)) 32 | assert_equal time_in_day(0, 0, 1, 29), every.next_at(time_in_day(23, 59, 0, 59)) 33 | end 34 | 35 | def test_duration_1s 36 | every = Zhong::Every.parse(1.second) 37 | 38 | assert_equal time_in_day(16, 20, 0, 1), every.next_at(time_in_day(16, 20)) 39 | assert_equal time_in_day(16, 20, 0, 11), every.next_at(time_in_day(16, 20, 0, 10)) 40 | assert_equal time_in_day(16, 20, 0, 16), every.next_at(time_in_day(16, 20, 0, 15)) 41 | assert_equal time_in_day(0, 0, 1, 0), every.next_at(time_in_day(23, 59, 0, 59)) 42 | end 43 | 44 | def test_duration_3_weeks 45 | every = Zhong::Every.parse(3.weeks) 46 | 47 | assert_equal time_in_day(16, 20, 21), every.next_at(time_in_day(16, 20)) 48 | assert_equal time_in_day(16, 20, 21, 10), every.next_at(time_in_day(16, 20, 0, 10)) 49 | assert_equal time_in_day(16, 20, 21, 15), every.next_at(time_in_day(16, 20, 0, 15)) 50 | assert_equal time_in_day(0, 0, 21, 10), every.next_at(time_in_day(0, 0, 0, 10)) 51 | end 52 | 53 | def test_symbol_day 54 | every = Zhong::Every.parse(:day) 55 | 56 | assert_equal time_in_day(16, 20, 1, 0), every.next_at(time_in_day(16, 20)) 57 | assert_equal time_in_day(16, 20, 1, 40), every.next_at(time_in_day(16, 20, 0, 40)) 58 | assert_equal time_in_day(16, 20, 1, 45), every.next_at(time_in_day(16, 20, 0, 45)) 59 | assert_equal time_in_day(0, 0, 1, 10), every.next_at(time_in_day(0, 0, 0, 10)) 60 | end 61 | 62 | def test_string_day 63 | every = Zhong::Every.parse("day") 64 | 65 | assert_equal time_in_day(16, 20, 1, 0), every.next_at(time_in_day(16, 20)) 66 | assert_equal time_in_day(16, 20, 1, 40), every.next_at(time_in_day(16, 20, 0, 40)) 67 | assert_equal time_in_day(16, 20, 1, 45), every.next_at(time_in_day(16, 20, 0, 45)) 68 | assert_equal time_in_day(0, 0, 1, 10), every.next_at(time_in_day(0, 0, 0, 10)) 69 | end 70 | 71 | def test_string_week 72 | every = Zhong::Every.parse("week") 73 | 74 | assert_equal time_in_day(16, 20, 7, 0), every.next_at(time_in_day(16, 20)) 75 | assert_equal time_in_day(16, 20, 7, 40), every.next_at(time_in_day(16, 20, 0, 40)) 76 | assert_equal time_in_day(16, 20, 7, 45), every.next_at(time_in_day(16, 20, 0, 45)) 77 | assert_equal time_in_day(0, 0, 7, 10), every.next_at(time_in_day(0, 0, 0, 10)) 78 | end 79 | 80 | def test_to_s 81 | assert_equal "3 minutes", Zhong::Every.parse(3.minute).to_s 82 | assert_equal "3 hours", Zhong::Every.parse(3.hour).to_s 83 | assert_equal "1 hour", Zhong::Every.parse(1.hour).to_s 84 | assert_equal "3 days", Zhong::Every.parse(3.day).to_s 85 | assert_equal "3 weeks", Zhong::Every.parse(3.week).to_s 86 | assert_equal "3 months", Zhong::Every.parse(3.month).to_s 87 | assert_equal "1 month", Zhong::Every.parse(1.month).to_s 88 | assert_equal "3 years", Zhong::Every.parse(3.year).to_s 89 | assert_equal "3 decades", Zhong::Every.parse(30.years).to_s 90 | end 91 | 92 | def test_invalid_string_foo 93 | assert_raises Zhong::Every::FailedToParse do 94 | Zhong::Every.parse("foo") 95 | end 96 | end 97 | 98 | def test_invalid_object 99 | assert_raises Zhong::Every::FailedToParse do 100 | Zhong::Every.parse(true) 101 | end 102 | end 103 | 104 | def test_nil_argument 105 | assert_raises Zhong::Every::FailedToParse do 106 | Zhong::Every.parse(nil) 107 | end 108 | end 109 | 110 | def test_invalid_blank 111 | assert_raises Zhong::Every::FailedToParse do 112 | Zhong::Every.parse("") 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | ecmaFeatures: 2 | modules: true 3 | jsx: true 4 | 5 | env: 6 | amd: true 7 | browser: true 8 | es6: true 9 | jquery: true 10 | node: true 11 | 12 | # http://eslint.org/docs/rules/ 13 | rules: 14 | # Possible Errors 15 | comma-dangle: [2, never] 16 | no-cond-assign: 2 17 | no-console: 0 18 | no-constant-condition: 2 19 | no-control-regex: 2 20 | no-debugger: 2 21 | no-dupe-args: 2 22 | no-dupe-keys: 2 23 | no-duplicate-case: 2 24 | no-empty: 2 25 | no-empty-character-class: 2 26 | no-ex-assign: 2 27 | no-extra-boolean-cast: 2 28 | no-extra-parens: 0 29 | no-extra-semi: 2 30 | no-func-assign: 2 31 | no-inner-declarations: [2, functions] 32 | no-invalid-regexp: 2 33 | no-irregular-whitespace: 2 34 | no-negated-in-lhs: 2 35 | no-obj-calls: 2 36 | no-regex-spaces: 2 37 | no-sparse-arrays: 2 38 | no-unexpected-multiline: 2 39 | no-unreachable: 2 40 | use-isnan: 2 41 | valid-jsdoc: 0 42 | valid-typeof: 2 43 | 44 | # Best Practices 45 | accessor-pairs: 2 46 | block-scoped-var: 0 47 | complexity: [2, 6] 48 | consistent-return: 0 49 | curly: 0 50 | default-case: 0 51 | dot-location: 0 52 | dot-notation: 0 53 | eqeqeq: 2 54 | guard-for-in: 2 55 | no-alert: 2 56 | no-caller: 2 57 | no-case-declarations: 2 58 | no-div-regex: 2 59 | no-else-return: 0 60 | no-empty-label: 2 61 | no-empty-pattern: 2 62 | no-eq-null: 2 63 | no-eval: 2 64 | no-extend-native: 2 65 | no-extra-bind: 2 66 | no-fallthrough: 2 67 | no-floating-decimal: 0 68 | no-implicit-coercion: 0 69 | no-implied-eval: 2 70 | no-invalid-this: 0 71 | no-iterator: 2 72 | no-labels: 0 73 | no-lone-blocks: 2 74 | no-loop-func: 2 75 | no-magic-number: 0 76 | no-multi-spaces: 0 77 | no-multi-str: 0 78 | no-native-reassign: 2 79 | no-new-func: 2 80 | no-new-wrappers: 2 81 | no-new: 2 82 | no-octal-escape: 2 83 | no-octal: 2 84 | no-proto: 2 85 | no-redeclare: 2 86 | no-return-assign: 2 87 | no-script-url: 2 88 | no-self-compare: 2 89 | no-sequences: 0 90 | no-throw-literal: 0 91 | no-unused-expressions: 2 92 | no-useless-call: 2 93 | no-useless-concat: 2 94 | no-void: 2 95 | no-warning-comments: 0 96 | no-with: 2 97 | radix: 2 98 | vars-on-top: 0 99 | wrap-iife: 2 100 | yoda: 0 101 | 102 | # Strict 103 | strict: 0 104 | 105 | # Variables 106 | init-declarations: 0 107 | no-catch-shadow: 2 108 | no-delete-var: 2 109 | no-label-var: 2 110 | no-shadow-restricted-names: 2 111 | no-shadow: 0 112 | no-undef-init: 2 113 | no-undef: 0 114 | no-undefined: 0 115 | no-unused-vars: 0 116 | no-use-before-define: 0 117 | 118 | # Node.js and CommonJS 119 | callback-return: 2 120 | global-require: 2 121 | handle-callback-err: 2 122 | no-mixed-requires: 0 123 | no-new-require: 0 124 | no-path-concat: 2 125 | no-process-exit: 2 126 | no-restricted-modules: 0 127 | no-sync: 0 128 | 129 | # Stylistic Issues 130 | array-bracket-spacing: 0 131 | block-spacing: 0 132 | brace-style: 0 133 | camelcase: 0 134 | comma-spacing: 0 135 | comma-style: 0 136 | computed-property-spacing: 0 137 | consistent-this: 0 138 | eol-last: 0 139 | func-names: 0 140 | func-style: 0 141 | id-length: 0 142 | id-match: 0 143 | indent: 0 144 | jsx-quotes: 0 145 | key-spacing: 0 146 | linebreak-style: 0 147 | lines-around-comment: 0 148 | max-depth: 0 149 | max-len: 0 150 | max-nested-callbacks: 0 151 | max-params: 0 152 | max-statements: [2, 30] 153 | new-cap: 0 154 | new-parens: 0 155 | newline-after-var: 0 156 | no-array-constructor: 0 157 | no-bitwise: 0 158 | no-continue: 0 159 | no-inline-comments: 0 160 | no-lonely-if: 0 161 | no-mixed-spaces-and-tabs: 0 162 | no-multiple-empty-lines: 0 163 | no-negated-condition: 0 164 | no-nested-ternary: 0 165 | no-new-object: 0 166 | no-plusplus: 0 167 | no-restricted-syntax: 0 168 | no-spaced-func: 0 169 | no-ternary: 0 170 | no-trailing-spaces: 0 171 | no-underscore-dangle: 0 172 | no-unneeded-ternary: 0 173 | object-curly-spacing: 0 174 | one-var: 0 175 | operator-assignment: 0 176 | operator-linebreak: 0 177 | padded-blocks: 0 178 | quote-props: 0 179 | quotes: 0 180 | require-jsdoc: 0 181 | semi-spacing: 0 182 | semi: 0 183 | sort-vars: 0 184 | space-after-keywords: 0 185 | space-before-blocks: 0 186 | space-before-function-paren: 0 187 | space-before-keywords: 0 188 | space-in-parens: 0 189 | space-infix-ops: 0 190 | space-return-throw-case: 0 191 | space-unary-ops: 0 192 | spaced-comment: 0 193 | wrap-regex: 0 194 | 195 | # ECMAScript 6 196 | arrow-body-style: 0 197 | arrow-parens: 0 198 | arrow-spacing: 0 199 | constructor-super: 0 200 | generator-star-spacing: 0 201 | no-arrow-condition: 0 202 | no-class-assign: 0 203 | no-const-assign: 0 204 | no-dupe-class-members: 0 205 | no-this-before-super: 0 206 | no-var: 0 207 | object-shorthand: 0 208 | prefer-arrow-callback: 0 209 | prefer-const: 0 210 | prefer-reflect: 0 211 | prefer-spread: 0 212 | prefer-template: 0 213 | require-yield: 0 214 | -------------------------------------------------------------------------------- /lib/zhong/job.rb: -------------------------------------------------------------------------------- 1 | module Zhong 2 | class Job 3 | extend Forwardable 4 | def_delegators Zhong, :redis, :tz, :logger, :heartbeat_key 5 | 6 | attr_reader :name, :category, :last_ran, :at, :every, :id 7 | 8 | def initialize(job_name, config = {}, callbacks = {}, &block) 9 | @name = job_name 10 | @category = config[:category] 11 | @logger = config[:logger] 12 | @config = config 13 | @callbacks = callbacks 14 | 15 | @at = config[:at] ? At.parse(config[:at], grace: config.fetch(:grace, 15.minutes)) : nil 16 | @every = config[:every] ? Every.parse(config[:every]) : nil 17 | 18 | raise "must specific either `at` or `every` for job: #{self}" unless @at || @every 19 | 20 | @block = block 21 | 22 | @if = config[:if] 23 | @long_running_timeout = config[:long_running_timeout] 24 | @running = false 25 | @first_run = true 26 | @last_ran = nil 27 | @id = Digest::SHA256.hexdigest(@name) 28 | end 29 | 30 | def run?(time = Time.now) 31 | if @first_run 32 | clear_last_ran_if_at_changed if @at 33 | refresh_last_ran 34 | @first_run = false 35 | end 36 | 37 | run_every?(time) && run_at?(time) && run_if?(time) 38 | end 39 | 40 | def run(time = Time.now, error_handler = nil) 41 | return unless run?(time) 42 | 43 | locked = false 44 | errored = false 45 | ran = false 46 | 47 | begin 48 | redis_lock.lock do 49 | locked = true 50 | @running = true 51 | 52 | refresh_last_ran 53 | 54 | # we need to check again, as another process might have acquired 55 | # the lock right before us and obviated our need to do anything 56 | break unless run?(time) 57 | 58 | if disabled? 59 | logger.info "not running, disabled: #{self}" 60 | break 61 | end 62 | 63 | logger.info "running: #{self}" 64 | 65 | if @block 66 | begin 67 | @block.call 68 | ran = true 69 | rescue => boom 70 | logger.error "#{self} failed: #{boom}" 71 | error_handler.call(boom, self) if error_handler 72 | end 73 | end 74 | 75 | ran!(time) 76 | end 77 | rescue Suo::LockClientError => boom 78 | logger.error "unable to run due to client error: #{boom}" 79 | errored = true 80 | end 81 | 82 | @running = false 83 | 84 | logger.info "unable to acquire exclusive run lock: #{self}" if !locked && !errored 85 | 86 | ran 87 | end 88 | 89 | def running? 90 | @running 91 | end 92 | 93 | def refresh_last_ran 94 | last_ran_val = redis.get(last_ran_key) 95 | @last_ran = last_ran_val ? Time.at(last_ran_val.to_i) : nil 96 | end 97 | 98 | def disable 99 | fire_callbacks(:before_disable, self) 100 | redis.set(disabled_key, "true") 101 | fire_callbacks(:after_disable, self) 102 | end 103 | 104 | def enable 105 | fire_callbacks(:before_enable, self) 106 | redis.del(disabled_key) 107 | fire_callbacks(:after_enable, self) 108 | end 109 | 110 | def disabled? 111 | !redis.get(disabled_key).nil? 112 | end 113 | 114 | def to_s 115 | @to_s ||= [@category, @name].compact.join(".").freeze 116 | end 117 | 118 | def next_at 119 | every_time = @every.next_at(@last_ran) if @last_ran && @every 120 | at_time = @at.next_at(Time.now) if @at 121 | [every_time, at_time, Time.now].compact.max || "now" 122 | end 123 | 124 | def clear 125 | redis.del(last_ran_key) 126 | end 127 | 128 | def last_ran_key 129 | "zhong:last_ran:#{self}" 130 | end 131 | 132 | def desired_at_key 133 | "zhong:at:#{self}" 134 | end 135 | 136 | def disabled_key 137 | "zhong:disabled:#{self}" 138 | end 139 | 140 | def lock_key 141 | "zhong:lock:#{self}" 142 | end 143 | 144 | private 145 | 146 | def fire_callbacks(event, *args) 147 | @callbacks[event].to_a.map do |callback| 148 | callback.call(*args) 149 | end.compact.all? # do not skip on nils 150 | end 151 | 152 | # if the @at value is changed across runs, the last_run becomes invalid 153 | # so clear it 154 | def clear_last_ran_if_at_changed 155 | previous_at_msgpack = redis.get(desired_at_key) 156 | 157 | if previous_at_msgpack 158 | previous_at = At.deserialize(previous_at_msgpack) 159 | 160 | if previous_at != @at 161 | logger.error "#{self} period changed (from #{previous_at} to #{@at}), clearing last run" 162 | clear 163 | end 164 | end 165 | 166 | redis.set(desired_at_key, @at.serialize) 167 | end 168 | 169 | def run_every?(time) 170 | !@last_ran || !@every || @every.next_at(@last_ran) <= time 171 | end 172 | 173 | def run_at?(time) 174 | !@at || @at.next_at(time) <= time 175 | end 176 | 177 | def run_if?(time) 178 | !@if || @if.call(time) 179 | end 180 | 181 | def ran!(time) 182 | @last_ran = time 183 | redis.set(last_ran_key, @last_ran.to_i) 184 | end 185 | 186 | def redis_lock 187 | @lock ||= Suo::Client::Redis.new(lock_key, client: redis, stale_lock_expiration: @long_running_timeout) 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /test/test_at.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | # Many At tests lifted from Clockwork (thanks Clockwork!) 4 | class TestAt < Minitest::Test 5 | def time_in_day(hour, minute, day = 0, sec = 0) 6 | Time.new.beginning_of_week(:monday).change(hour: hour, min: minute, sec: sec) + day.days 7 | end 8 | 9 | def test_16_20 10 | at = Zhong::At.parse("16:20", grace: 0) 11 | 12 | assert_equal time_in_day(16, 20), at.next_at(time_in_day(16, 15)) 13 | assert_equal time_in_day(16, 20), at.next_at(time_in_day(16, 20)) 14 | assert_equal time_in_day(16, 20), at.next_at(time_in_day(16, 20, 0, 10)) 15 | assert_equal time_in_day(16, 20), at.next_at(time_in_day(16, 20, 0, 59)) 16 | assert_equal time_in_day(16, 20, 1), at.next_at(time_in_day(16, 21)) 17 | assert_equal time_in_day(16, 20, 1), at.next_at(time_in_day(16, 21, 0, 01)) 18 | assert_equal time_in_day(16, 20, 2), at.next_at(time_in_day(16, 21, 1, 30)) 19 | end 20 | 21 | def test_16_20_with_grace 22 | at = Zhong::At.parse("16:20", grace: 5.minutes) 23 | 24 | assert_equal time_in_day(16, 20), at.next_at(time_in_day(16, 21)) 25 | assert_equal time_in_day(16, 20), at.next_at(time_in_day(16, 25)) 26 | assert_equal time_in_day(16, 20, 1), at.next_at(time_in_day(16, 26)) 27 | end 28 | 29 | def test_8_20_before 30 | at = Zhong::At.parse("8:20") 31 | 32 | assert_equal time_in_day(8, 20), at.next_at(time_in_day(8, 15)) 33 | assert_equal time_in_day(8, 20, 1), at.next_at(time_in_day(8, 21)) 34 | end 35 | 36 | def test_two_star_20 37 | at = Zhong::At.parse("**:20") 38 | 39 | assert_equal time_in_day(8, 20), at.next_at(time_in_day(8, 15)) 40 | assert_equal time_in_day(9, 20), at.next_at(time_in_day(9, 15)) 41 | assert_equal time_in_day(10, 20), at.next_at(time_in_day(9, 45)) 42 | end 43 | 44 | def test_one_star_20 45 | at = Zhong::At.parse("*:45") 46 | 47 | assert_equal time_in_day(8, 45), at.next_at(time_in_day(8, 35)) 48 | assert_equal time_in_day(9, 45), at.next_at(time_in_day(9, 35)) 49 | assert_equal time_in_day(10, 45), at.next_at(time_in_day(9, 50)) 50 | end 51 | 52 | def test_one_star_20_with_grace 53 | at = Zhong::At.parse("*:45", grace: 5.minutes) 54 | 55 | assert_equal time_in_day(8, 45), at.next_at(time_in_day(8, 35)) 56 | assert_equal time_in_day(8, 45), at.next_at(time_in_day(8, 50)) 57 | assert_equal time_in_day(9, 45), at.next_at(time_in_day(8, 51)) 58 | assert_equal time_in_day(9, 45), at.next_at(time_in_day(9, 35)) 59 | assert_equal time_in_day(10, 45), at.next_at(time_in_day(9, 55)) 60 | end 61 | 62 | def test_16_two_stars 63 | at = Zhong::At.parse("16:**") 64 | 65 | assert_equal time_in_day(16, 00), at.next_at(time_in_day(8, 45)) 66 | assert_equal time_in_day(16, 00), at.next_at(time_in_day(10, 00)) 67 | assert_equal time_in_day(16, 00), at.next_at(time_in_day(16, 00)) 68 | assert_equal time_in_day(16, 01), at.next_at(time_in_day(16, 01)) 69 | assert_equal time_in_day(16, 30), at.next_at(time_in_day(16, 30)) 70 | assert_equal time_in_day(16, 59), at.next_at(time_in_day(16, 59)) 71 | assert_equal time_in_day(16, 00, 1), at.next_at(time_in_day(17, 00)) 72 | assert_equal time_in_day(16, 00, 1), at.next_at(time_in_day(23, 59)) 73 | end 74 | 75 | def test_8_two_stars 76 | at = Zhong::At.parse("8:**") 77 | 78 | assert_equal time_in_day(8, 00), at.next_at(time_in_day(3, 45)) 79 | assert_equal time_in_day(8, 00), at.next_at(time_in_day(5, 00)) 80 | assert_equal time_in_day(8, 00), at.next_at(time_in_day(8, 00)) 81 | assert_equal time_in_day(8, 01), at.next_at(time_in_day(8, 01)) 82 | assert_equal time_in_day(8, 30), at.next_at(time_in_day(8, 30)) 83 | assert_equal time_in_day(8, 59), at.next_at(time_in_day(8, 59)) 84 | assert_equal time_in_day(8, 00, 1), at.next_at(time_in_day(9, 00)) 85 | assert_equal time_in_day(8, 00, 1), at.next_at(time_in_day(12, 59)) 86 | end 87 | 88 | def test_saturday_12 89 | at = Zhong::At.parse("Saturday 12:00") 90 | 91 | assert_equal time_in_day(12, 00, 5), at.next_at(time_in_day(12, 00)) 92 | assert_equal time_in_day(12, 00, 5), at.next_at(time_in_day(13, 00, 1)) 93 | assert_equal time_in_day(12, 00, 5), at.next_at(time_in_day(23, 59, 3)) 94 | assert_equal time_in_day(12, 00, 5), at.next_at(time_in_day(12, 00, 5)) 95 | assert_equal time_in_day(12, 00, 12), at.next_at(time_in_day(12, 01, 5)) 96 | assert_equal time_in_day(12, 00, 12), at.next_at(time_in_day(13, 01, 6, 01)) 97 | end 98 | 99 | def test_sat_12 100 | at = Zhong::At.parse("sat 12:00") 101 | 102 | assert_equal time_in_day(12, 00, 5), at.next_at(time_in_day(12, 00)) 103 | assert_equal time_in_day(12, 00, 5), at.next_at(time_in_day(13, 00, 1)) 104 | assert_equal time_in_day(12, 00, 5), at.next_at(time_in_day(23, 59, 3)) 105 | assert_equal time_in_day(12, 00, 5), at.next_at(time_in_day(12, 00, 5)) 106 | assert_equal time_in_day(12, 00, 12), at.next_at(time_in_day(12, 01, 5)) 107 | assert_equal time_in_day(12, 00, 12), at.next_at(time_in_day(13, 01, 6, 01)) 108 | end 109 | 110 | def test_tue_12_59 111 | at = Zhong::At.parse("tue 12:59") 112 | 113 | assert_equal time_in_day(12, 59, 1), at.next_at(time_in_day(7, 00)) 114 | assert_equal time_in_day(12, 59, 1), at.next_at(time_in_day(12, 00, 1)) 115 | assert_equal time_in_day(12, 59, 1), at.next_at(time_in_day(12, 59, 1)) 116 | assert_equal time_in_day(12, 59, 8), at.next_at(time_in_day(13, 00, 1)) 117 | assert_equal time_in_day(12, 59, 8), at.next_at(time_in_day(13, 01, 6, 01)) 118 | end 119 | 120 | def test_thr_12_59 121 | at = Zhong::At.parse("thr 12:59") 122 | 123 | assert_equal time_in_day(12, 59, 3), at.next_at(time_in_day(7, 00)) 124 | assert_equal time_in_day(12, 59, 3), at.next_at(time_in_day(12, 00, 3)) 125 | assert_equal time_in_day(12, 59, 3), at.next_at(time_in_day(12, 59, 3)) 126 | assert_equal time_in_day(12, 59, 10), at.next_at(time_in_day(13, 00, 3)) 127 | assert_equal time_in_day(12, 59, 10), at.next_at(time_in_day(13, 01, 6, 01)) 128 | end 129 | 130 | def test_multi_at 131 | at = Zhong::At.parse(["8:20", "tues 12:30"]) 132 | 133 | assert_equal time_in_day(8, 20), at.next_at(time_in_day(8, 15)) 134 | assert_equal time_in_day(8, 20), at.next_at(time_in_day(8, 20)) 135 | assert_equal time_in_day(8, 20, 1), at.next_at(time_in_day(8, 21)) 136 | assert_equal time_in_day(12, 30, 1), at.next_at(time_in_day(8, 21, 1)) 137 | assert_equal time_in_day(12, 30, 1), at.next_at(time_in_day(12, 30, 1)) 138 | assert_equal time_in_day(8, 20, 2), at.next_at(time_in_day(12, 31, 1)) 139 | assert_equal time_in_day(8, 20, 3), at.next_at(time_in_day(12, 31, 2)) 140 | end 141 | 142 | def test_to_s 143 | at = Zhong::At.parse("thr 12:59") 144 | 145 | assert_equal "12:59 on Thr", at.to_s 146 | 147 | at = Zhong::At.parse(["8:20", "tues 12:30"]) 148 | 149 | assert_equal "08:20, 12:30 on Tues", at.to_s 150 | end 151 | 152 | def test_as_json 153 | at = Zhong::At.parse("tues 23:01") 154 | 155 | assert_equal({m: 1, h: 23, w: 2, g: 0.seconds}, at.as_json) 156 | 157 | at = Zhong::At.parse(["8:**", "sun 12:30"]) 158 | 159 | assert_equal([{m: nil, h: 8, w: nil, g: 0.seconds}, {m: 30, h: 12, w: 0, g: 0.seconds}], at.as_json) 160 | end 161 | 162 | def test_serialize_multi 163 | at = Zhong::At.parse(["8:20", "tues 12:30"]) 164 | 165 | assert_equal Zhong::At.deserialize(at.serialize), at 166 | end 167 | 168 | def test_serialize_single 169 | at = Zhong::At.parse("sun 14:20") 170 | 171 | assert_equal Zhong::At.deserialize(at.serialize), at 172 | end 173 | 174 | def test_invalid_time_32 175 | assert_raises Zhong::At::FailedToParse do 176 | Zhong::At.parse("32:00") 177 | end 178 | end 179 | 180 | def test_invalid_time_caturday 181 | assert_raises Zhong::At::FailedToParse do 182 | Zhong::At.parse("caturday 12:00") 183 | end 184 | end 185 | 186 | def test_invalid_multi_at 187 | assert_raises Zhong::At::FailedToParse do 188 | Zhong::At.parse(["12:*", "31:00"]) 189 | end 190 | end 191 | 192 | def test_invalid_multi_line_time_sat_12 193 | assert_raises Zhong::At::FailedToParse do 194 | Zhong::At.parse("sat 12:00\nreally invalid time") 195 | end 196 | end 197 | 198 | def test_invalid_nil 199 | assert_raises Zhong::At::FailedToParse do 200 | Zhong::At.parse(nil) 201 | end 202 | end 203 | 204 | def test_invalid_blank 205 | assert_raises Zhong::At::FailedToParse do 206 | Zhong::At.parse("") 207 | end 208 | end 209 | end 210 | -------------------------------------------------------------------------------- /web/assets/javascript/vendor.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.0.0 | (c) jQuery Foundation | jquery.org/license */ 2 | !function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.0.0",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:f.call(this)},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=r.isArray(d)))?(e?(e=!1,f=c&&r.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return a&&"[object Object]"===k.call(a)?(b=e(a))?(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n):!0:!1},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;d>f;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;return"string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a)?(d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e):void 0},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"===c||r.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\x00-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,ca=function(a,b){return b?"\x00"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"label"in b&&b.disabled===a||"form"in b&&b.disabled===a||"form"in b&&b.disabled===!1&&(b.isDisabled===a||b.isDisabled!==!a&&("label"in b||!ea(b))!==a)}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[0>c?c+b:c]}),even:pa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e)}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,e>i&&ya(a.slice(i,e)),f>e&&ya(a=a.slice(e)),f>e&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(_,aa),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=V.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(_,aa),$.test(j[0].type)&&qa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&sa(j),!a)return G.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||$.test(a)&&qa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){if(r.isFunction(b))return r.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return r.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(C.test(b))return r.filter(b,a,c);b=r.filter(b,a)}return r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType})}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;d>b;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;d>b;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/\S+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(f>b)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,M,e),g(f,c,N,e)):(f++,j.call(a,g(f,c,M,e),g(f,c,N,e),g(f,c,M,c.notifyWith))):(d!==M&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(1>=b&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){ 3 | return j.call(r(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},T=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function U(){this.expando=r.expando+U.uid++}U.uid=1,U.prototype={cache:function(a){var b=a[this.expando];return b||(b={},T(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){r.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(K)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var V=new U,W=new U,X=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Y=/[A-Z]/g;function Z(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Y,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:X.test(c)?JSON.parse(c):c}catch(e){}W.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return W.hasData(a)||V.hasData(a)},data:function(a,b,c){return W.access(a,b,c)},removeData:function(a,b){W.remove(a,b)},_data:function(a,b,c){return V.access(a,b,c)},_removeData:function(a,b){V.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=W.get(f),1===f.nodeType&&!V.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),Z(f,d,e[d])));V.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){W.set(this,a)}):S(this,function(b){var c;if(f&&void 0===b){if(c=W.get(f,a),void 0!==c)return c;if(c=Z(f,a),void 0!==c)return c}else this.each(function(){W.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthf;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=V.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&ba(d)&&(e[f]=fa(d))):"none"!==c&&(e[f]="none",V.set(d,"display",c)));for(f=0;g>f;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ga(this,!0)},hide:function(){return ga(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){ba(this)?r(this).show():r(this).hide()})}});var ha=/^(?:checkbox|radio)$/i,ia=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,ja=/^$|\/(?:java|ecma)script/i,ka={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ka.optgroup=ka.option,ka.tbody=ka.tfoot=ka.colgroup=ka.caption=ka.thead,ka.th=ka.td;function la(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function ma(a,b){for(var c=0,d=a.length;d>c;c++)V.set(a[c],"globalEval",!b||V.get(b[c],"globalEval"))}var na=/<|&#?\w+;/;function oa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;o>n;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(na.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ia.exec(f)||["",""])[1].toLowerCase(),i=ka[h]||ka._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=la(l.appendChild(f),"script"),j&&ma(g),c){k=0;while(f=g[k++])ja.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var pa=d.documentElement,qa=/^key/,ra=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,sa=/^([^.]*)(?:\.(.+)|)/;function ta(){return!0}function ua(){return!1}function va(){try{return d.activeElement}catch(a){}}function wa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)wa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ua;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(pa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;cc;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?r(e,this).index(i)>-1:r.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h\x20\t\r\n\f]*)[^>]*)\/>/gi,ya=/\s*$/g;function Ca(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Da(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ea(a){var b=Aa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)r.event.add(b,e,j[e][c])}W.hasData(a)&&(h=W.access(a),i=r.extend({},h),W.set(b,i))}}function Ga(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ha.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ha(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&za.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(m&&(e=oa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(la(e,"script"),Da),i=h.length;m>l;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,la(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Ea),l=0;i>l;l++)j=h[l],ja.test(j.type||"")&&!V.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Ba,""),k))}return a}function Ia(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(la(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&ma(la(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(xa,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=la(h),f=la(a),d=0,e=f.length;e>d;d++)Ga(f[d],g[d]);if(b)if(c)for(f=f||la(a),g=g||la(h),d=0,e=f.length;e>d;d++)Fa(f[d],g[d]);else Fa(a,h);return g=la(h,"script"),g.length>0&&ma(g,!i&&la(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(la(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!ya.test(a)&&!ka[(ia.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(la(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(la(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;f>=g;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var Ja=/^margin/,Ka=new RegExp("^("+$+")(?!px)[a-z%]+$","i"),La=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",pa.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,pa.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Ma(a,b,c){var d,e,f,g,h=a.style;return c=c||La(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&Ka.test(g)&&Ja.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Na(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Oa=/^(none|table(?!-c[ea]).+)/,Pa={position:"absolute",visibility:"hidden",display:"block"},Qa={letterSpacing:"0",fontWeight:"400"},Ra=["Webkit","Moz","ms"],Sa=d.createElement("div").style;function Ta(a){if(a in Sa)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ra.length;while(c--)if(a=Ra[c]+b,a in Sa)return a}function Ua(a,b,c){var d=_.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Va(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=r.css(a,c+aa[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+aa[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+aa[f]+"Width",!0,e))):(g+=r.css(a,"padding"+aa[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+aa[f]+"Width",!0,e)));return g}function Wa(a,b,c){var d,e=!0,f=La(a),g="border-box"===r.css(a,"boxSizing",!1,f);if(a.getClientRects().length&&(d=a.getBoundingClientRect()[b]),0>=d||null==d){if(d=Ma(a,b,f),(0>d||null==d)&&(d=a.style[b]),Ka.test(d))return d;e=g&&(o.boxSizingReliable()||d===a.style[b]),d=parseFloat(d)||0}return d+Va(a,b,c||(g?"border":"content"),e,f)+"px"}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Ma(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=a.style;return b=r.cssProps[h]||(r.cssProps[h]=Ta(h)||h),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=_.exec(c))&&e[1]&&(c=da(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b);return b=r.cssProps[h]||(r.cssProps[h]=Ta(h)||h),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Ma(a,b,d)),"normal"===e&&b in Qa&&(e=Qa[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){return c?!Oa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?Wa(a,b,d):ca(a,Pa,function(){return Wa(a,b,d)}):void 0},set:function(a,c,d){var e,f=d&&La(a),g=d&&Va(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=_.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Ua(a,c,g)}}}),r.cssHooks.marginLeft=Na(o.reliableMarginLeft,function(a,b){return b?(parseFloat(Ma(a,"marginLeft"))||a.getBoundingClientRect().left-ca(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px":void 0}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+aa[d]+b]=f[d]||f[d-2]||f[0];return e}},Ja.test(a)||(r.cssHooks[a+b].set=Ua)}),r.fn.extend({css:function(a,b){return S(this,function(a,b,c){var d,e,f={},g=0;if(r.isArray(b)){for(d=La(a),e=b.length;e>g;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function Xa(a,b,c,d,e){return new Xa.prototype.init(a,b,c,d,e)}r.Tween=Xa,Xa.prototype={constructor:Xa,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Xa.propHooks[this.prop];return a&&a.get?a.get(this):Xa.propHooks._default.get(this)},run:function(a){var b,c=Xa.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Xa.propHooks._default.set(this),this}},Xa.prototype.init.prototype=Xa.prototype,Xa.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Xa.propHooks.scrollTop=Xa.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Xa.prototype.init,r.fx.step={};var Ya,Za,$a=/^(?:toggle|show|hide)$/,_a=/queueHooks$/;function ab(){Za&&(a.requestAnimationFrame(ab),r.fx.tick())}function bb(){return a.setTimeout(function(){Ya=void 0}),Ya=r.now()}function cb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=aa[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function db(a,b,c){for(var d,e=(gb.tweeners[b]||[]).concat(gb.tweeners["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function eb(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&ba(a),q=V.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],$a.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmptyObject(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=V.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ga([a],!0),j=a.style.display||j,k=r.css(a,"display"),ga([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=V.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ga([a],!0),m.done(function(){p||ga([a]),V.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=db(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function fb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],r.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function gb(a,b,c){var d,e,f=0,g=gb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Ya||bb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:Ya||bb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(fb(k,j.opts.specialEasing);g>f;f++)if(d=gb.prefilters[f].call(j,a,k,j.opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,db,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}r.Animation=r.extend(gb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return da(c.elem,a,_.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(K);for(var c,d=0,e=a.length;e>d;d++)c=a[d],gb.tweeners[c]=gb.tweeners[c]||[],gb.tweeners[c].unshift(b)},prefilters:[eb],prefilter:function(a,b){b?gb.prefilters.unshift(a):gb.prefilters.push(a)}}),r.speed=function(a,b,c){var e=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off||d.hidden?e.duration=0:e.duration="number"==typeof e.duration?e.duration:e.duration in r.fx.speeds?r.fx.speeds[e.duration]:r.fx.speeds._default,null!=e.queue&&e.queue!==!0||(e.queue="fx"),e.old=e.complete,e.complete=function(){r.isFunction(e.old)&&e.old.call(this),e.queue&&r.dequeue(this,e.queue)},e},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(ba).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=gb(this,r.extend({},a),f);(e||V.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=V.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&_a.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=V.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(cb(b,!0),a,d,e)}}),r.each({slideDown:cb("show"),slideUp:cb("hide"),slideToggle:cb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(Ya=r.now();b1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?hb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c); 4 | }}),hb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=ib[b]||r.find.attr;ib[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=ib[g],ib[g]=e,e=null!=c(a,b,d)?g:null,ib[g]=f),e}});var jb=/^(?:input|select|textarea|button)$/i,kb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):jb.test(a.nodeName)||kb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});var lb=/[\t\r\n\f]/g;function mb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,mb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,mb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,mb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=mb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(c)+" ").replace(lb," ").indexOf(b)>-1)return!0;return!1}});var nb=/\r/g,ob=/[\x20\t\r\n\f]+/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(nb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:r.trim(r.text(a)).replace(ob," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],(c.selected||i===e)&&!c.disabled&&(!c.parentNode.disabled||!r.nodeName(c.parentNode,"optgroup"))){if(b=r(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){return r.isArray(b)?a.checked=r.inArray(r(a).val(),b)>-1:void 0}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?r.event.trigger(a,b,c,!0):void 0}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ha.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,""),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&300>b||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",0>b&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;return o.cors||Pb&&!b.crossDomain?{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}:void 0}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("